package org.apereo.cas.web.flow.resolver.impl; import com.google.common.base.Throwables; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.AuthenticationException; import org.apereo.cas.authentication.AuthenticationResult; import org.apereo.cas.authentication.AuthenticationResultBuilder; import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.HandlerResult; import org.apereo.cas.authentication.MessageDescriptor; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.services.MultifactorAuthenticationProvider; import org.apereo.cas.services.MultifactorAuthenticationProviderSelector; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.RegisteredServiceMultifactorPolicy; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.registry.TicketRegistrySupport; import org.apereo.cas.web.flow.CasWebflowConstants; import org.apereo.cas.web.flow.resolver.CasWebflowEventResolver; import org.apereo.cas.web.support.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.util.CookieGenerator; import org.springframework.webflow.action.EventFactorySupport; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.core.collection.LocalAttributeMap; import org.springframework.webflow.definition.TransitionDefinition; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; /** * This is {@link AbstractCasWebflowEventResolver} that provides parent * operations for all child event resolvers to handle core webflow changes. * * @author Misagh Moayyed * @since 5.0.0 */ public abstract class AbstractCasWebflowEventResolver implements CasWebflowEventResolver { private static final String RESOLVED_AUTHENTICATION_EVENTS = "resolvedAuthenticationEvents"; private static final String DEFAULT_MESSAGE_BUNDLE_PREFIX = "authenticationFailure."; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractCasWebflowEventResolver.class); /** * CAS event publisher. */ @Autowired protected ApplicationEventPublisher eventPublisher; /** * The Application context. */ @Autowired protected ConfigurableApplicationContext applicationContext; /** * The Authentication system support. */ protected final AuthenticationSystemSupport authenticationSystemSupport; /** * Ticket registry support. */ protected final TicketRegistrySupport ticketRegistrySupport; /** * The Services manager. */ protected final ServicesManager servicesManager; /** * The Central authentication service. */ protected final CentralAuthenticationService centralAuthenticationService; /** * Warn cookie generator. */ protected final CookieGenerator warnCookieGenerator; /** * The mfa selector. */ protected final MultifactorAuthenticationProviderSelector multifactorAuthenticationProviderSelector; /** * Extract the service specially in the event that it's proxied by a callback. */ protected final AuthenticationServiceSelectionPlan authenticationRequestServiceSelectionStrategies; public AbstractCasWebflowEventResolver(final AuthenticationSystemSupport authenticationSystemSupport, final CentralAuthenticationService centralAuthenticationService, final ServicesManager servicesManager, final TicketRegistrySupport ticketRegistrySupport, final CookieGenerator warnCookieGenerator, final AuthenticationServiceSelectionPlan authenticationSelectionStrategies, final MultifactorAuthenticationProviderSelector selector) { this.authenticationSystemSupport = authenticationSystemSupport; this.centralAuthenticationService = centralAuthenticationService; this.servicesManager = servicesManager; this.ticketRegistrySupport = ticketRegistrySupport; this.warnCookieGenerator = warnCookieGenerator; authenticationRequestServiceSelectionStrategies = authenticationSelectionStrategies; multifactorAuthenticationProviderSelector = selector; } /** * Adds a warning message to the message context. * * @param context Message context. * @param warning Warning message. */ protected static void addMessageDescriptorToMessageContext(final MessageContext context, final MessageDescriptor warning) { final MessageBuilder builder = new MessageBuilder() .warning() .code(warning.getCode()) .defaultText(warning.getDefaultMessage()) .args((Object[]) warning.getParams()); context.addMessage(builder.build()); } /** * New event based on the given id. * * @param id the id * @return the event */ protected Event newEvent(final String id) { return new Event(this, id); } /** * Add warning messages to message context if needed. * * @param tgtId the tgt id * @param messageContext the message context * @return true if warnings were found and added, false otherwise. * @since 4.1.0 */ private static boolean addWarningMessagesToMessageContextIfNeeded(final TicketGrantingTicket tgtId, final MessageContext messageContext) { boolean foundAndAddedWarnings = false; for (final Map.Entry<String, HandlerResult> entry : tgtId.getAuthentication().getSuccesses().entrySet()) { for (final MessageDescriptor message : entry.getValue().getWarnings()) { addMessageDescriptorToMessageContext(messageContext, message); foundAndAddedWarnings = true; } } return foundAndAddedWarnings; } /** * New event based on the id, which contains an error attribute referring to the exception occurred. * * @param id the id * @param error the error * @return the event */ protected Event newEvent(final String id, final Exception error) { return new Event(this, id, new LocalAttributeMap(CasWebflowConstants.TRANSITION_ID_ERROR, error)); } /** * Gets credential from context. * * @param context the context * @return the credential from context */ protected Credential getCredentialFromContext(final RequestContext context) { return WebUtils.getCredential(context); } /** * Grant ticket granting ticket. * * @param context the context * @param authenticationResultBuilder the authentication result builder * @param service the service * @return the event * @throws Exception the exception */ protected Event grantTicketGrantingTicketToAuthenticationResult(final RequestContext context, final AuthenticationResultBuilder authenticationResultBuilder, final Service service) throws Exception { LOGGER.debug("Finalizing authentication transactions and issuing ticket-granting ticket"); final AuthenticationResult authenticationResult = this.authenticationSystemSupport.finalizeAllAuthenticationTransactions(authenticationResultBuilder, service); final Authentication authentication = authenticationResult.getAuthentication(); final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context); final TicketGrantingTicket tgt = createOrUpdateTicketGrantingTicket(authenticationResult, authentication, ticketGrantingTicket); WebUtils.putTicketGrantingTicketInScopes(context, tgt); WebUtils.putAuthenticationResult(authenticationResult, context); WebUtils.putAuthentication(tgt.getAuthentication(), context); if (addWarningMessagesToMessageContextIfNeeded(tgt, context.getMessageContext())) { return newEvent(CasWebflowConstants.TRANSITION_ID_SUCCESS_WITH_WARNINGS); } return newEvent(CasWebflowConstants.TRANSITION_ID_SUCCESS); } private TicketGrantingTicket createOrUpdateTicketGrantingTicket(final AuthenticationResult authenticationResult, final Authentication authentication, final String ticketGrantingTicket) { final TicketGrantingTicket tgt; if (shouldIssueTicketGrantingTicket(authentication, ticketGrantingTicket)) { tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult); } else { tgt = this.centralAuthenticationService.getTicket(ticketGrantingTicket, TicketGrantingTicket.class); tgt.getAuthentication().update(authentication); this.centralAuthenticationService.updateTicket(tgt); } return tgt; } private boolean shouldIssueTicketGrantingTicket(final Authentication authentication, final String ticketGrantingTicket) { boolean issueTicketGrantingTicket = true; if (StringUtils.isNotBlank(ticketGrantingTicket)) { LOGGER.debug("Located ticket-granting ticket in the context. Retrieving associated authentication"); final Authentication authenticationFromTgt = this.ticketRegistrySupport.getAuthenticationFrom(ticketGrantingTicket); if (authenticationFromTgt == null) { LOGGER.debug("Authentication session associated with [{}] is no longer valid", ticketGrantingTicket); this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket); } else if (authentication.getPrincipal().equals(authenticationFromTgt.getPrincipal())) { LOGGER.debug("Resulting authentication matches the authentication from context"); issueTicketGrantingTicket = false; } else { LOGGER.debug("Resulting authentication is different from the context"); } } return issueTicketGrantingTicket; } /** * Gets authentication provider for service. * * @param service the service * @return the authentication provider for service */ protected Collection<MultifactorAuthenticationProvider> getAuthenticationProviderForService(final RegisteredService service) { final RegisteredServiceMultifactorPolicy policy = service.getMultifactorPolicy(); if (policy != null) { return policy.getMultifactorAuthenticationProviders().stream() .map(this::getMultifactorAuthenticationProviderFromApplicationContext) .filter(Optional::isPresent).map(Optional::get) .collect(Collectors.toSet()); } return null; } /** * Validate event for transition. * * @param eventId the event id * @param context the context * @param attributes the attributes * @return the event */ protected Event validateEventIdForMatchingTransitionInContext(final String eventId, final RequestContext context, final Map<String, Object> attributes) { try { final AttributeMap<Object> attributesMap = new LocalAttributeMap<>(attributes); final Event event = new Event(this, eventId, attributesMap); LOGGER.debug("Resulting event id is [{}]. Locating transitions in the context for that event id...", event.getId()); final TransitionDefinition def = context.getMatchingTransition(event.getId()); if (def == null) { LOGGER.warn("Transition definition cannot be found for event [{}]", event.getId()); throw new AuthenticationException(); } LOGGER.debug("Found matching transition [{}] with target [{}] for event [{}] with attributes [{}].", def.getId(), def.getTargetStateId(), event.getId(), event.getAttributes()); return event; } catch (final Exception e) { throw Throwables.propagate(e); } } /** * Build event attribute map map. * * @param principal the principal * @param service the service * @param provider the provider * @return the map */ protected static Map<String, Object> buildEventAttributeMap(final Principal principal, final RegisteredService service, final MultifactorAuthenticationProvider provider) { final Map<String, Object> map = new HashMap<>(); map.put(Principal.class.getName(), principal); map.put(RegisteredService.class.getName(), service); map.put(MultifactorAuthenticationProvider.class.getName(), provider); return map; } private Set<Event> resolveEventViaMultivaluedAttribute(final Principal principal, final Object attributeValue, final RegisteredService service, final RequestContext context, final MultifactorAuthenticationProvider provider, final Predicate<String> predicate) { final Set<Event> events = new HashSet<>(); if (attributeValue instanceof Collection) { LOGGER.debug("Attribute value [{}] is a multi-valued attribute", attributeValue); final Collection<String> values = (Collection<String>) attributeValue; values.forEach(value -> { try { if (predicate.test(value)) { LOGGER.debug("Attribute value predicate [{}] has successfully matched the [{}]", predicate, value); LOGGER.debug("Attempting to verify multifactor authentication provider [{}] for [{}]", provider, service); if (provider.isAvailable(service)) { LOGGER.debug("Provider [{}] is successfully verified", provider); final String id = provider.getId(); final Event event = validateEventIdForMatchingTransitionInContext(id, context, buildEventAttributeMap(principal, service, provider)); events.add(event); } } else { LOGGER.debug("Attribute value predicate [{}] could not match the [{}]", predicate, value); } } catch (final Exception e) { LOGGER.debug("Ignoring [{}] since no matching transition could be found", value); } }); return events; } LOGGER.debug("Attribute value [{}] of type [{}] is not a multi-valued attribute", attributeValue, attributeValue.getClass()); return null; } private Set<Event> resolveEventViaSingleAttribute(final Principal principal, final Object attributeValue, final RegisteredService service, final RequestContext context, final MultifactorAuthenticationProvider provider, final Predicate<String> predicate) { try { if (attributeValue instanceof String) { LOGGER.debug("Attribute value [{}] is a single-valued attribute", attributeValue); if (predicate.test((String) attributeValue)) { LOGGER.debug("Attribute value predicate [{}] has matched the [{}]", predicate, attributeValue); LOGGER.debug("Attempting to isAvailable multifactor authentication provider [{}] for [{}]", provider, service); if (provider.isAvailable(service)) { LOGGER.debug("Provider [{}] is successfully verified", provider); final String id = provider.getId(); final Event event = validateEventIdForMatchingTransitionInContext(id, context, buildEventAttributeMap(principal, service, provider)); return Collections.singleton(event); } LOGGER.debug("Provider [{}] could not be verified", provider); } else { LOGGER.debug("Attribute value predicate [{}] could not match the [{}]", predicate, attributeValue); } } } catch (final Exception e) { throw Throwables.propagate(e); } LOGGER.debug("Attribute value [{}] is not a single-valued attribute", attributeValue); return null; } private Set<Event> resolveEventViaAttribute(final Principal principal, final Map<String, Object> attributesToExamine, final Collection<String> attributeNames, final RegisteredService service, final RequestContext context, final Collection<MultifactorAuthenticationProvider> providers, final Predicate<String> predicate) { if (providers == null || providers.isEmpty()) { LOGGER.debug("No authentication provider is associated with this service"); return null; } LOGGER.debug("Locating attribute value for attribute(s): [{}]", attributeNames); for (final String attributeName : attributeNames) { final Object attributeValue = attributesToExamine.get(attributeName); if (attributeValue == null) { LOGGER.debug("Attribute value for [{}] to determine event is not configured for [{}]", attributeName, principal.getId()); continue; } LOGGER.debug("Selecting a multifactor authentication provider out of [{}] for [{}] and service [{}]", providers, principal.getId(), service); final MultifactorAuthenticationProvider provider = this.multifactorAuthenticationProviderSelector.resolve(providers, service, principal); LOGGER.debug("Located attribute value [{}] for [{}]", attributeValue, attributeNames); Set<Event> results = resolveEventViaSingleAttribute(principal, attributeValue, service, context, provider, predicate); if (results == null || results.isEmpty()) { results = resolveEventViaMultivaluedAttribute(principal, attributeValue, service, context, provider, predicate); } if (results != null && !results.isEmpty()) { LOGGER.debug("Resolved set of events based on the attribute [{}] are [{}]", attributeName, results); return results; } } LOGGER.debug("No set of events based on the attribute(s) [{}] could be matched", attributeNames); return null; } /** * Resolve event via authentication attribute set. * * @param authentication the authentication * @param attributeNames the attribute name * @param service the service * @param context the context * @param providers the providers * @param predicate the predicate * @return the set of resolved events */ protected Set<Event> resolveEventViaAuthenticationAttribute(final Authentication authentication, final Collection<String> attributeNames, final RegisteredService service, final RequestContext context, final Collection<MultifactorAuthenticationProvider> providers, final Predicate<String> predicate) { return resolveEventViaAttribute(authentication.getPrincipal(), authentication.getAttributes(), attributeNames, service, context, providers, predicate); } /** * Resolve event via principal attribute set. * * @param principal the principal * @param attributeNames the attribute name * @param service the service * @param context the context * @param providers the providers * @param predicate the predicate * @return the set of resolved events */ protected Set<Event> resolveEventViaPrincipalAttribute(final Principal principal, final Collection<String> attributeNames, final RegisteredService service, final RequestContext context, final Collection<MultifactorAuthenticationProvider> providers, final Predicate<String> predicate) { return resolveEventViaAttribute(principal, principal.getAttributes(), attributeNames, service, context, providers, predicate); } @Override public Set<Event> resolve(final RequestContext context) { WebUtils.putWarnCookieIfRequestParameterPresent(this.warnCookieGenerator, context); WebUtils.putPublicWorkstationToFlowIfRequestParameterPresent(context); return resolveInternal(context); } @Override public Event resolveSingle(final RequestContext context) { final Set<Event> events = resolve(context); if (events == null || events.isEmpty()) { return null; } final Event event = events.iterator().next(); LOGGER.debug("Resolved single event [{}] via [{}] for this context", event.getId(), event.getSource().getClass().getName()); return event; } /** * Find the MultifactorAuthenticationProvider in the application contact that matches the specified providerId (e.g. "mfa-duo"). * * @param providerId the provider id * @return the registered service multifactor authentication provider */ protected Optional<MultifactorAuthenticationProvider> getMultifactorAuthenticationProviderFromApplicationContext(final String providerId) { try { LOGGER.debug("Locating bean definition for [{}]", providerId); return this.applicationContext.getBeansOfType(MultifactorAuthenticationProvider.class, false, true).values().stream() .filter(p -> p.matches(providerId)) .findFirst(); } catch (final Exception e) { LOGGER.debug("Could not locate [{}] bean id in the application context as an authentication provider.", providerId); } return Optional.empty(); } /** * Put resolved events as attribute. * * @param context the context * @param resolvedEvents the resolved events */ protected void putResolvedEventsAsAttribute(final RequestContext context, final Set<Event> resolvedEvents) { context.getAttributes().put(RESOLVED_AUTHENTICATION_EVENTS, resolvedEvents); } /** * Resolve service from authentication request. * * @param service the service * @return the service */ protected Service resolveServiceFromAuthenticationRequest(final Service service) { return this.authenticationRequestServiceSelectionStrategies.resolveService(service); } /** * Resolve service from authentication request service. * * @param context the context * @return the service */ protected Service resolveServiceFromAuthenticationRequest(final RequestContext context) { final Service ctxService = WebUtils.getService(context); return resolveServiceFromAuthenticationRequest(ctxService); } /** * Gets resolved events as attribute. * * @param context the context * @return the resolved events as attribute */ protected Set<Event> getResolvedEventsAsAttribute(final RequestContext context) { return context.getAttributes().get(RESOLVED_AUTHENTICATION_EVENTS, Set.class); } /** * Handle authentication transaction and grant ticket granting ticket. * * @param context the context * @return the set */ protected Set<Event> handleAuthenticationTransactionAndGrantTicketGrantingTicket(final RequestContext context) { try { final Credential credential = getCredentialFromContext(context); AuthenticationResultBuilder builder = WebUtils.getAuthenticationResultBuilder(context); LOGGER.debug("Handling authentication transaction for credential [{}]", credential); final Service service = WebUtils.getService(context); builder = this.authenticationSystemSupport.handleAuthenticationTransaction(service, builder, credential); LOGGER.debug("Issuing ticket-granting tickets for service [{}]", service); return Collections.singleton(grantTicketGrantingTicketToAuthenticationResult(context, builder, service)); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); final MessageContext messageContext = context.getMessageContext(); messageContext.addMessage(new MessageBuilder().error() .code(DEFAULT_MESSAGE_BUNDLE_PREFIX.concat(e.getClass().getSimpleName())).build()); return Collections.singleton(new EventFactorySupport().error(this)); } } }