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));
}
}
}