package org.apereo.cas;
import com.codahale.metrics.annotation.Counted;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.Timed;
import org.apereo.cas.authentication.Authentication;
import org.apereo.cas.authentication.AuthenticationBuilder;
import org.apereo.cas.authentication.AuthenticationException;
import org.apereo.cas.authentication.AuthenticationResult;
import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan;
import org.apereo.cas.authentication.ContextualAuthenticationPolicyFactory;
import org.apereo.cas.authentication.AuthenticationCredentialsLocalBinder;
import org.apereo.cas.authentication.DefaultAuthenticationBuilder;
import org.apereo.cas.authentication.exceptions.MixedPrincipalException;
import org.apereo.cas.authentication.PrincipalException;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.Service;
import org.apereo.cas.logout.LogoutManager;
import org.apereo.cas.logout.LogoutRequest;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.RegisteredServiceAccessStrategyUtils;
import org.apereo.cas.services.RegisteredServiceAttributeReleasePolicy;
import org.apereo.cas.services.ServiceContext;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.services.UnauthorizedProxyingException;
import org.apereo.cas.services.UnauthorizedSsoServiceException;
import org.apereo.cas.support.events.ticket.CasProxyGrantingTicketCreatedEvent;
import org.apereo.cas.support.events.ticket.CasProxyTicketGrantedEvent;
import org.apereo.cas.support.events.ticket.CasServiceTicketGrantedEvent;
import org.apereo.cas.support.events.ticket.CasServiceTicketValidatedEvent;
import org.apereo.cas.support.events.ticket.CasTicketGrantingTicketCreatedEvent;
import org.apereo.cas.support.events.ticket.CasTicketGrantingTicketDestroyedEvent;
import org.apereo.cas.ticket.AbstractTicketException;
import org.apereo.cas.ticket.InvalidTicketException;
import org.apereo.cas.ticket.ServiceTicket;
import org.apereo.cas.ticket.ServiceTicketFactory;
import org.apereo.cas.ticket.TicketFactory;
import org.apereo.cas.ticket.TicketGrantingTicket;
import org.apereo.cas.ticket.TicketGrantingTicketFactory;
import org.apereo.cas.ticket.UnrecognizableServiceForServiceTicketValidationException;
import org.apereo.cas.ticket.proxy.ProxyGrantingTicket;
import org.apereo.cas.ticket.proxy.ProxyGrantingTicketFactory;
import org.apereo.cas.ticket.proxy.ProxyTicket;
import org.apereo.cas.ticket.proxy.ProxyTicketFactory;
import org.apereo.cas.ticket.registry.TicketRegistry;
import org.apereo.cas.validation.Assertion;
import org.apereo.cas.validation.ImmutableAssertion;
import org.apereo.inspektr.audit.annotation.Audit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Concrete implementation of a {@link CentralAuthenticationService}, and also the
* central, organizing component of CAS' internal implementation.
* This class is threadsafe.
*
* @author William G. Thompson, Jr.
* @author Scott Battaglia
* @author Dmitry Kopylenko
* @author Misagh Moayyed
* @since 3.0.0
*/
@Transactional(transactionManager = "ticketTransactionManager")
public class DefaultCentralAuthenticationService extends AbstractCentralAuthenticationService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCentralAuthenticationService.class);
private static final long serialVersionUID = -8943828074939533986L;
/**
* Build the central authentication service implementation.
*
* @param ticketRegistry the tickets registry.
* @param ticketFactory the ticket factory
* @param servicesManager the services manager.
* @param logoutManager the logout manager.
* @param selectionStrategies The service selection strategy during validation events.
* @param authenticationPolicyFactory Authentication policy that uses a service context to
* produce stateful security policies to apply when authenticating credentials.
* @param principalFactory principal factory to create principal objects
* @param cipherExecutor Cipher executor to handle ticket validation.
*/
public DefaultCentralAuthenticationService(final TicketRegistry ticketRegistry,
final TicketFactory ticketFactory,
final ServicesManager servicesManager,
final LogoutManager logoutManager,
final AuthenticationServiceSelectionPlan selectionStrategies,
final ContextualAuthenticationPolicyFactory<ServiceContext> authenticationPolicyFactory,
final PrincipalFactory principalFactory,
final CipherExecutor<String, String> cipherExecutor) {
super(ticketRegistry, ticketFactory, servicesManager, logoutManager,
selectionStrategies, authenticationPolicyFactory,
principalFactory, cipherExecutor);
}
@Audit(
action = "TICKET_GRANTING_TICKET_DESTROYED",
actionResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOLVER",
resourceResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
@Timed(name = "DESTROY_TICKET_GRANTING_TICKET_TIMER")
@Metered(name = "DESTROY_TICKET_GRANTING_TICKET_METER")
@Counted(name = "DESTROY_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
@Override
public List<LogoutRequest> destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
try {
LOGGER.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
LOGGER.debug("Ticket found. Processing logout requests and then deleting the ticket...");
AuthenticationCredentialsLocalBinder.bindCurrent(ticket.getAuthentication());
final List<LogoutRequest> logoutRequests = this.logoutManager.performLogout(ticket);
deleteTicket(ticketGrantingTicketId);
doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
return logoutRequests;
} catch (final InvalidTicketException e) {
LOGGER.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
}
return Collections.emptyList();
}
@Audit(
action = "SERVICE_TICKET",
actionResolverName = "GRANT_SERVICE_TICKET_RESOLVER",
resourceResolverName = "GRANT_SERVICE_TICKET_RESOURCE_RESOLVER")
@Timed(name = "GRANT_SERVICE_TICKET_TIMER")
@Metered(name = "GRANT_SERVICE_TICKET_METER")
@Counted(name = "GRANT_SERVICE_TICKET_COUNTER", monotonic = true)
@Override
public ServiceTicket grantServiceTicket(final String ticketGrantingTicketId, final Service service, final AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException {
final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, ticketGrantingTicket);
final Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(authenticationResult, ticketGrantingTicket);
RegisteredServiceAccessStrategyUtils.ensureServiceSsoAccessIsAllowed(registeredService, service, ticketGrantingTicket);
evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService);
// Perform security policy check by getting the authentication that satisfies the configured policy
// This throws if no suitable policy is found
getAuthenticationSatisfiedByPolicy(currentAuthentication, new ServiceContext(service, registeredService));
final Authentication latestAuthentication = ticketGrantingTicket.getRoot().getAuthentication();
AuthenticationCredentialsLocalBinder.bindCurrent(latestAuthentication);
final Principal principal = latestAuthentication.getPrincipal();
final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service,
authenticationResult != null && authenticationResult.isCredentialProvided());
this.ticketRegistry.updateTicket(ticketGrantingTicket);
this.ticketRegistry.addTicket(serviceTicket);
LOGGER.info("Granted ticket [{}] for service [{}] and principal [{}]",
serviceTicket.getId(), service.getId(), principal.getId());
doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket));
return serviceTicket;
}
@Audit(
action = "PROXY_TICKET",
actionResolverName = "GRANT_PROXY_TICKET_RESOLVER",
resourceResolverName = "GRANT_PROXY_TICKET_RESOURCE_RESOLVER")
@Timed(name = "GRANT_PROXY_TICKET_TIMER")
@Metered(name = "GRANT_PROXY_TICKET_METER")
@Counted(name = "GRANT_PROXY_TICKET_COUNTER", monotonic = true)
@Override
public ProxyTicket grantProxyTicket(final String proxyGrantingTicket, final Service service)
throws AbstractTicketException {
final ProxyGrantingTicket proxyGrantingTicketObject = getTicket(proxyGrantingTicket, ProxyGrantingTicket.class);
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
try {
RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service,
registeredService, proxyGrantingTicketObject);
RegisteredServiceAccessStrategyUtils.ensureServiceSsoAccessIsAllowed(registeredService, service, proxyGrantingTicketObject);
} catch (final PrincipalException e) {
throw new UnauthorizedSsoServiceException();
}
evaluateProxiedServiceIfNeeded(service, proxyGrantingTicketObject, registeredService);
// Perform security policy check by getting the authentication that satisfies the configured policy
// This throws if no suitable policy is found
getAuthenticationSatisfiedByPolicy(proxyGrantingTicketObject.getRoot().getAuthentication(),
new ServiceContext(service, registeredService));
final Authentication authentication = proxyGrantingTicketObject.getRoot().getAuthentication();
AuthenticationCredentialsLocalBinder.bindCurrent(authentication);
final Principal principal = authentication.getPrincipal();
final ProxyTicketFactory factory = this.ticketFactory.get(ProxyTicket.class);
final ProxyTicket proxyTicket = factory.create(proxyGrantingTicketObject, service);
this.ticketRegistry.updateTicket(proxyGrantingTicketObject);
this.ticketRegistry.addTicket(proxyTicket);
LOGGER.info("Granted ticket [{}] for service [{}] for user [{}]",
proxyTicket.getId(), service.getId(), principal.getId());
doPublishEvent(new CasProxyTicketGrantedEvent(this, proxyGrantingTicketObject, proxyTicket));
return proxyTicket;
}
@Audit(
action = "PROXY_GRANTING_TICKET",
actionResolverName = "CREATE_PROXY_GRANTING_TICKET_RESOLVER",
resourceResolverName = "CREATE_PROXY_GRANTING_TICKET_RESOURCE_RESOLVER")
@Timed(name = "CREATE_PROXY_GRANTING_TICKET_TIMER")
@Metered(name = "CREATE_PROXY_GRANTING_TICKET_METER")
@Counted(name = "CREATE_PROXY_GRANTING_TICKET_COUNTER", monotonic = true)
@Override
public ProxyGrantingTicket createProxyGrantingTicket(final String serviceTicketId, final AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException {
AuthenticationCredentialsLocalBinder.bindCurrent(authenticationResult.getAuthentication());
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
if (serviceTicket == null || serviceTicket.isExpired()) {
LOGGER.debug("ServiceTicket [{}] has expired or cannot be found in the ticket registry", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
final RegisteredService registeredService = this.servicesManager.findServiceBy(serviceTicket.getService());
RegisteredServiceAccessStrategyUtils
.ensurePrincipalAccessIsAllowedForService(serviceTicket, authenticationResult, registeredService);
if (!registeredService.getProxyPolicy().isAllowedToProxy()) {
LOGGER.warn("ServiceManagement: Service [{}] attempted to proxy, but is not allowed.", serviceTicket.getService().getId());
throw new UnauthorizedProxyingException();
}
final Authentication authentication = authenticationResult.getAuthentication();
final ProxyGrantingTicketFactory factory = this.ticketFactory.get(ProxyGrantingTicket.class);
final ProxyGrantingTicket proxyGrantingTicket = factory.create(serviceTicket, authentication);
LOGGER.debug("Generated proxy granting ticket [{}] based off of [{}]", proxyGrantingTicket, serviceTicketId);
this.ticketRegistry.addTicket(proxyGrantingTicket);
doPublishEvent(new CasProxyGrantingTicketCreatedEvent(this, proxyGrantingTicket));
return proxyGrantingTicket;
}
@Audit(
action = "SERVICE_TICKET_VALIDATE",
actionResolverName = "VALIDATE_SERVICE_TICKET_RESOLVER",
resourceResolverName = "VALIDATE_SERVICE_TICKET_RESOURCE_RESOLVER")
@Timed(name = "VALIDATE_SERVICE_TICKET_TIMER")
@Metered(name = "VALIDATE_SERVICE_TICKET_METER")
@Counted(name = "VALIDATE_SERVICE_TICKET_COUNTER", monotonic = true)
@Override
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
if (!isTicketAuthenticityVerified(serviceTicketId)) {
LOGGER.info("Service ticket [{}] is not a valid ticket issued by CAS.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
if (serviceTicket == null) {
LOGGER.warn("Service ticket [{}] does not exist.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
try {
/*
* Synchronization on ticket object in case of cache based registry doesn't serialize
* access to critical section. The reason is that cache pulls serialized data and
* builds new object, most likely for each pull. Is this synchronization needed here?
*/
synchronized (serviceTicket) {
if (serviceTicket.isExpired()) {
LOGGER.info("ServiceTicket [{}] has expired.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
if (!serviceTicket.isValidFor(service)) {
LOGGER.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
serviceTicketId, serviceTicket.getService().getId(), service);
throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
}
}
final Service selectedService = resolveServiceFromAuthenticationRequest(service);
LOGGER.debug("Resolved service [{}] from the authentication request", selectedService);
final RegisteredService registeredService = this.servicesManager.findServiceBy(selectedService);
LOGGER.debug("Located registered service definition [{}] from [{}] to handle validation request",
registeredService, selectedService);
RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(selectedService, registeredService);
final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot();
final Authentication authentication = getAuthenticationSatisfiedByPolicy(
root.getAuthentication(), new ServiceContext(selectedService, registeredService));
final Principal principal = authentication.getPrincipal();
final RegisteredServiceAttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy();
LOGGER.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService);
final Map<String, Object> attributesToRelease = attributePolicy != null
? attributePolicy.getAttributes(principal, selectedService, registeredService) : new HashMap<>();
final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, selectedService, registeredService);
final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease);
final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
builder.setPrincipal(modifiedPrincipal);
final Authentication finalAuthentication = builder.build();
AuthenticationCredentialsLocalBinder.bindCurrent(finalAuthentication);
final Assertion assertion = new ImmutableAssertion(
finalAuthentication,
serviceTicket.getGrantingTicket().getChainedAuthentications(),
selectedService,
serviceTicket.isFromNewLogin());
doPublishEvent(new CasServiceTicketValidatedEvent(this, serviceTicket, assertion));
return assertion;
} finally {
if (serviceTicket.isExpired()) {
deleteTicket(serviceTicketId);
} else {
this.ticketRegistry.updateTicket(serviceTicket);
}
}
}
@Audit(
action = "TICKET_GRANTING_TICKET",
actionResolverName = "CREATE_TICKET_GRANTING_TICKET_RESOLVER",
resourceResolverName = "CREATE_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
@Timed(name = "CREATE_TICKET_GRANTING_TICKET_TIMER")
@Metered(name = "CREATE_TICKET_GRANTING_TICKET_METER")
@Counted(name = "CREATE_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
@Override
public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException {
final Authentication authentication = authenticationResult.getAuthentication();
final Service service = authenticationResult.getService();
AuthenticationCredentialsLocalBinder.bindCurrent(authentication);
if (service != null) {
final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, authentication);
}
final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);
this.ticketRegistry.addTicket(ticketGrantingTicket);
doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));
return ticketGrantingTicket;
}
private static Authentication evaluatePossibilityOfMixedPrincipals(final AuthenticationResult context, final TicketGrantingTicket ticketGrantingTicket)
throws MixedPrincipalException {
Authentication currentAuthentication = null;
if (context != null) {
currentAuthentication = context.getAuthentication();
if (currentAuthentication != null) {
final Authentication original = ticketGrantingTicket.getAuthentication();
if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
throw new MixedPrincipalException(
currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
}
}
}
return currentAuthentication;
}
}