package org.apereo.cas; import com.codahale.metrics.annotation.Counted; import com.codahale.metrics.annotation.Metered; import com.codahale.metrics.annotation.Timed; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; 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.CurrentCredentialsAndAuthentication; import org.apereo.cas.authentication.DefaultAuthenticationBuilder; import org.apereo.cas.authentication.MixedPrincipalException; import org.apereo.cas.authentication.PrincipalException; import org.apereo.cas.authentication.principal.Principal; 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.CasProxyGrantingTicketCreatedEvent; import org.apereo.cas.support.events.CasProxyTicketGrantedEvent; import org.apereo.cas.support.events.CasServiceTicketGrantedEvent; import org.apereo.cas.support.events.CasServiceTicketValidatedEvent; import org.apereo.cas.support.events.CasTicketGrantingTicketCreatedEvent; import org.apereo.cas.support.events.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.cas.validation.ValidationServiceSelectionStrategy; import org.apereo.inspektr.audit.annotation.Audit; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; /** * @author Tanbin * @date 2018-12-12 */ @Transactional(readOnly = false, transactionManager = "ticketTransactionManager") public class CentralAuthenticationServiceImpl extends AbstractCentralAuthenticationService { private static final long serialVersionUID = -8943828074939533986L; public CentralAuthenticationServiceImpl() {} public CentralAuthenticationServiceImpl(TicketRegistry ticketRegistry, TicketFactory ticketFactory, ServicesManager servicesManager, LogoutManager logoutManager) { super(ticketRegistry, ticketFactory, servicesManager, logoutManager); } @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 destroyTicketGrantingTicket(String ticketGrantingTicketId) { try { this.logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId); TicketGrantingTicket ticket = (TicketGrantingTicket)getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); this.logger.debug("Ticket found. Processing logout requests and then deleting the ticket..."); CurrentCredentialsAndAuthentication.bindCurrent(ticket.getAuthentication()); List logoutRequests = this.logoutManager.performLogout(ticket); this.ticketRegistry.deleteTicket(ticketGrantingTicketId); doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket)); return logoutRequests; } catch (InvalidTicketException e) { this.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(String ticketGrantingTicketId, Service service, AuthenticationResult authenticationResult) throws AuthenticationException, AbstractTicketException { TicketGrantingTicket ticketGrantingTicket = (TicketGrantingTicket)getTicket(ticketGrantingTicketId, TicketGrantingTicket.class); RegisteredService registeredService = this.servicesManager.findServiceBy(service); RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, ticketGrantingTicket); Authentication currentAuthentication = evaluatePossibilityOfMixedPrincipals(authenticationResult, ticketGrantingTicket); RegisteredServiceAccessStrategyUtils.ensureServiceSsoAccessIsAllowed(registeredService, service, ticketGrantingTicket); evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService); getAuthenticationSatisfiedByPolicy(currentAuthentication, new ServiceContext(service, registeredService)); List authentications = ticketGrantingTicket.getChainedAuthentications(); Authentication latestAuthentication = (Authentication)authentications.get(authentications.size() - 1); CurrentCredentialsAndAuthentication.bindCurrent(latestAuthentication); Principal principal = latestAuthentication.getPrincipal(); ServiceTicketFactory factory = (ServiceTicketFactory)this.ticketFactory.get(ServiceTicket.class); ServiceTicket serviceTicket = (ServiceTicket)factory.create(ticketGrantingTicket, service, (authenticationResult != null && authenticationResult .isCredentialProvided())); this.ticketRegistry.updateTicket(ticketGrantingTicket); this.ticketRegistry.addTicket(serviceTicket); this.logger.info("Granted ticket [{}] for service [{}] and principal [{}]", new Object[] { serviceTicket .getId(), service.getId(), principal.getId() }); doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket)); return serviceTicket; } private static Authentication evaluatePossibilityOfMixedPrincipals(AuthenticationResult context, TicketGrantingTicket ticketGrantingTicket) throws MixedPrincipalException { Authentication currentAuthentication = null; if (context != null) { currentAuthentication = context.getAuthentication(); if (currentAuthentication != null) { Authentication original = ticketGrantingTicket.getAuthentication(); if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) { throw new MixedPrincipalException(currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal()); } } } return currentAuthentication; } @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(String proxyGrantingTicket, Service service) throws AbstractTicketException { ProxyGrantingTicket proxyGrantingTicketObject = (ProxyGrantingTicket)getTicket(proxyGrantingTicket, ProxyGrantingTicket.class); RegisteredService registeredService = this.servicesManager.findServiceBy(service); try { RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, proxyGrantingTicketObject); RegisteredServiceAccessStrategyUtils.ensureServiceSsoAccessIsAllowed(registeredService, service, proxyGrantingTicketObject); } catch (PrincipalException e) { throw new UnauthorizedSsoServiceException(); } evaluateProxiedServiceIfNeeded(service, proxyGrantingTicketObject, registeredService); getAuthenticationSatisfiedByPolicy(proxyGrantingTicketObject.getRoot().getAuthentication(), new ServiceContext(service, registeredService)); List authentications = proxyGrantingTicketObject.getChainedAuthentications(); Authentication authentication = (Authentication)authentications.get(authentications.size() - 1); CurrentCredentialsAndAuthentication.bindCurrent(authentication); Principal principal = authentication.getPrincipal(); ProxyTicketFactory factory = (ProxyTicketFactory)this.ticketFactory.get(ProxyTicket.class); ProxyTicket proxyTicket = (ProxyTicket)factory.create(proxyGrantingTicketObject, service); this.ticketRegistry.updateTicket(proxyGrantingTicketObject); this.ticketRegistry.addTicket(proxyTicket); this.logger.info("Granted ticket [{}] for service [{}] for user [{}]", new Object[] { 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(String serviceTicketId, AuthenticationResult authenticationResult) throws AuthenticationException, AbstractTicketException { CurrentCredentialsAndAuthentication.bindCurrent(authenticationResult.getAuthentication()); ServiceTicket serviceTicket = (ServiceTicket)this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class); if (serviceTicket == null || serviceTicket.isExpired()) { this.logger.debug("ServiceTicket [{}] has expired or cannot be found in the ticket registry", serviceTicketId); throw new InvalidTicketException(serviceTicketId); } RegisteredService registeredService = this.servicesManager.findServiceBy(serviceTicket.getService()); RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(serviceTicket, authenticationResult, registeredService); if (!registeredService.getProxyPolicy().isAllowedToProxy()) { this.logger.warn("ServiceManagement: Service [{}] attempted to proxy, but is not allowed.", serviceTicket.getService().getId()); throw new UnauthorizedProxyingException(); } Authentication authentication = authenticationResult.getAuthentication(); ProxyGrantingTicketFactory factory = (ProxyGrantingTicketFactory)this.ticketFactory.get(ProxyGrantingTicket.class); ProxyGrantingTicket proxyGrantingTicket = factory.create(serviceTicket, authentication); this.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(String serviceTicketId, Service service) throws AbstractTicketException { if (!ticketAuthenticityIsVerified(serviceTicketId)) { this.logger.info("Service ticket [{}] is not a valid ticket issued by CAS.", serviceTicketId); throw new InvalidTicketException(serviceTicketId); } ServiceTicket serviceTicket = (ServiceTicket)this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class); if (serviceTicket == null) { this.logger.info("Service ticket [{}] does not exist.", serviceTicketId); throw new InvalidTicketException(serviceTicketId); } try { synchronized (serviceTicket) { if (serviceTicket.isExpired()) { this.logger.info("ServiceTicket [{}] has expired.", serviceTicketId); throw new InvalidTicketException(serviceTicketId); } if (!serviceTicket.isValidFor(service)) { this.logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]", new Object[] { serviceTicketId, serviceTicket .getService().getId(), service }); throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService()); } } Service selectedService = ((ValidationServiceSelectionStrategy)this.validationServiceSelectionStrategies.stream().sorted().filter(s -> s.supports(service)).findFirst().get()).resolveServiceFrom(service); RegisteredService registeredService = this.servicesManager.findServiceBy(selectedService); this.logger.debug("Located registered service definition {} from {} to handle validation request", registeredService, selectedService); RegisteredServiceAccessStrategyUtils.ensureServiceAccessIsAllowed(selectedService, registeredService); TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot(); Authentication authentication = getAuthenticationSatisfiedByPolicy(root .getAuthentication(), new ServiceContext(selectedService, registeredService)); Principal principal = authentication.getPrincipal(); RegisteredServiceAttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy(); this.logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService); Map attributesToRelease = (attributePolicy != null) ? attributePolicy.getAttributes(principal) : new HashMap(5); String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, selectedService); Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease); AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication); builder.setPrincipal(modifiedPrincipal); Authentication finalAuthentication = builder.build(); CurrentCredentialsAndAuthentication.bindCurrent(finalAuthentication); ImmutableAssertion immutableAssertion = new ImmutableAssertion(finalAuthentication, serviceTicket.getGrantingTicket().getChainedAuthentications(), selectedService, serviceTicket.isFromNewLogin()); doPublishEvent(new CasServiceTicketValidatedEvent(this, serviceTicket, immutableAssertion)); return immutableAssertion; } finally { if (serviceTicket.isExpired()) { this.ticketRegistry.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(AuthenticationResult authenticationResult) throws AuthenticationException, AbstractTicketException { Authentication authentication = authenticationResult.getAuthentication(); Service service = authenticationResult.getService(); CurrentCredentialsAndAuthentication.bindCurrent(authentication); if (service != null) { RegisteredService registeredService = this.servicesManager.findServiceBy(service); RegisteredServiceAccessStrategyUtils.ensurePrincipalAccessIsAllowedForService(service, registeredService, authentication); } TicketGrantingTicketFactory factory = (TicketGrantingTicketFactory)this.ticketFactory.get(TicketGrantingTicket.class); TicketGrantingTicket ticketGrantingTicket = factory.create(authentication); this.ticketRegistry.addTicket(ticketGrantingTicket); doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket)); return ticketGrantingTicket; } private boolean ticketAuthenticityIsVerified(String ticketId) { if (this.cipherExecutor != null) { this.logger.debug("Attempting to decode service ticket {} to verify authenticity", ticketId); return !StringUtils.isEmpty(this.cipherExecutor.decode(ticketId)); } return !StringUtils.isEmpty(ticketId); } }