package org.apereo.cas.authentication; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apereo.cas.services.MultifactorAuthenticationProvider; import org.apereo.cas.services.MultifactorAuthenticationProviderBypass; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.RegisteredServiceMultifactorPolicy; import org.apereo.cas.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.OrderComparator; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Optional; /** * The {@link DefaultAuthenticationContextValidator} is responsible for evaluating an authentication * object to see whether it satisfied a requested authentication context. * * @author Misagh Moayyed * @since 4.3 */ public class DefaultAuthenticationContextValidator implements AuthenticationContextValidator { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAuthenticationContextValidator.class); private final String authenticationContextAttribute; private final String globalFailureMode; private final String mfaTrustedAuthnAttributeName; @Autowired private ConfigurableApplicationContext applicationContext; public DefaultAuthenticationContextValidator(final String contextAttribute, final String failureMode, final String authnAttributeName) { this.authenticationContextAttribute = contextAttribute; this.globalFailureMode = failureMode; this.mfaTrustedAuthnAttributeName = authnAttributeName; } public String getAuthenticationContextAttribute() { return this.authenticationContextAttribute; } /** * {@inheritDoc} * If the authentication event is established as part trusted/device browser * such that MFA was skipped, allow for validation to execute successfully. * If authentication event did bypass MFA, let for allow for validation to execute successfully. * * @param authentication the authentication * @param requestedContext the requested context * @param service the service * @return true if the context can be successfully validated. */ @Override public Pair<Boolean, Optional<MultifactorAuthenticationProvider>> validate(final Authentication authentication, final String requestedContext, final RegisteredService service) { final Map<String, Object> attrs = authentication.getAttributes(); final Object ctxAttr = attrs.get(this.authenticationContextAttribute); final Collection<Object> contexts = CollectionUtils.toCollection(ctxAttr); LOGGER.debug("Attempting to match requested authentication context [{}] against [{}]", requestedContext, contexts); final Map<String, MultifactorAuthenticationProvider> providerMap = getAllMultifactorAuthenticationProvidersFromApplicationContext(); if (providerMap == null) { LOGGER.debug("No providers have been configured"); return Pair.of(Boolean.FALSE, Optional.empty()); } final Optional<MultifactorAuthenticationProvider> requestedProvider = locateRequestedProvider(providerMap.values(), requestedContext); if (!requestedProvider.isPresent()) { LOGGER.debug("Requested authentication provider cannot be recognized."); return Pair.of(Boolean.FALSE, Optional.empty()); } if (contexts.stream().filter(ctx -> ctx.toString().equals(requestedContext)).count() > 0) { LOGGER.debug("Requested authentication context [{}] is satisfied", requestedContext); return Pair.of(Boolean.TRUE, requestedProvider); } if (StringUtils.isNotBlank(this.mfaTrustedAuthnAttributeName) && attrs.containsKey(this.mfaTrustedAuthnAttributeName)) { LOGGER.debug("Requested authentication context [{}] is satisfied since device is already trusted", requestedContext); return Pair.of(Boolean.TRUE, requestedProvider); } if (attrs.containsKey(MultifactorAuthenticationProviderBypass.AUTHENTICATION_ATTRIBUTE_BYPASS_MFA) && attrs.containsKey(MultifactorAuthenticationProviderBypass.AUTHENTICATION_ATTRIBUTE_BYPASS_MFA_PROVIDER)) { final boolean isBypass = Boolean.class.cast(attrs.get(MultifactorAuthenticationProviderBypass.AUTHENTICATION_ATTRIBUTE_BYPASS_MFA)); final String bypassedId = attrs.get(MultifactorAuthenticationProviderBypass.AUTHENTICATION_ATTRIBUTE_BYPASS_MFA_PROVIDER).toString(); LOGGER.debug("Found multifactor authentication bypass attributes for provider [{}]", bypassedId); if (isBypass && StringUtils.equals(bypassedId, requestedContext)) { LOGGER.debug("Requested authentication context [{}] is satisfied given mfa was bypassed for the authentication attempt", requestedContext); return Pair.of(Boolean.TRUE, requestedProvider); } LOGGER.debug("Either multifactor authentication was not bypassed or the requested context [{}] does not match the bypassed provider [{}]", requestedProvider, bypassedId); } final Collection<MultifactorAuthenticationProvider> satisfiedProviders = getSatisfiedAuthenticationProviders(authentication, providerMap.values()); if (satisfiedProviders == null) { LOGGER.warn("No satisfied multifactor authentication providers are recorded in the current authentication context."); return Pair.of(Boolean.FALSE, requestedProvider); } if (!satisfiedProviders.isEmpty()) { final MultifactorAuthenticationProvider[] providers = satisfiedProviders.toArray(new MultifactorAuthenticationProvider[]{}); OrderComparator.sortIfNecessary(providers); final Optional<MultifactorAuthenticationProvider> result = Arrays.stream(providers) .filter(provider -> { final MultifactorAuthenticationProvider p = requestedProvider.get(); return provider.equals(p) || provider.getOrder() >= p.getOrder(); }) .findFirst(); if (result.isPresent()) { LOGGER.debug("Current provider [{}] already satisfies the authentication requirements of [{}]; proceed with flow normally.", result.get(), requestedProvider); return Pair.of(Boolean.TRUE, requestedProvider); } } LOGGER.debug("No multifactor providers could be located to satisfy the requested context for [{}]", requestedProvider); final RegisteredServiceMultifactorPolicy.FailureModes mode = getMultifactorFailureModeForService(service); if (mode == RegisteredServiceMultifactorPolicy.FailureModes.PHANTOM) { if (!requestedProvider.get().isAvailable(service)) { LOGGER.debug("Service [{}] is configured to use a [{}] failure mode for multifactor authentication policy. " + "Since provider [{}] is unavailable at the moment, CAS will knowingly allow [{}] as a satisfied criteria " + "of the present authentication context", service.getServiceId(), mode, requestedProvider, requestedContext); return Pair.of(true, requestedProvider); } } if (mode == RegisteredServiceMultifactorPolicy.FailureModes.OPEN) { if (!requestedProvider.get().isAvailable(service)) { LOGGER.debug("Service [{}] is configured to use a [{}] failure mode for multifactor authentication policy and " + "since provider [{}] is unavailable at the moment, CAS will consider the authentication satisfied " + "without the presence of [{}]", service.getServiceId(), mode, requestedProvider, requestedContext); return Pair.of(true, satisfiedProviders.stream().findFirst()); } } return Pair.of(false, requestedProvider); } /** * Gets all multifactor authentication providers from application context. * * @return the all multifactor authentication providers from application context */ private Map<String, MultifactorAuthenticationProvider> getAllMultifactorAuthenticationProvidersFromApplicationContext() { try { return this.applicationContext.getBeansOfType(MultifactorAuthenticationProvider.class, false, true); } catch (final Exception e) { LOGGER.warn("Could not locate beans of type [{}] in the application context", MultifactorAuthenticationProvider.class); } return null; } private Collection<MultifactorAuthenticationProvider> getSatisfiedAuthenticationProviders(final Authentication authentication, final Collection<MultifactorAuthenticationProvider> providers) { final Collection<Object> contexts = CollectionUtils.toCollection( authentication.getAttributes().get(this.authenticationContextAttribute)); if (contexts == null || contexts.isEmpty()) { LOGGER.debug("No authentication context could be determined based on authentication attribute [{}]", this.authenticationContextAttribute); return null; } contexts.stream().forEach(context -> providers.removeIf(provider -> !provider.getId().equals(context)) ); LOGGER.debug("Found [{}] providers that may satisfy the context", providers.size()); return providers; } private static Optional<MultifactorAuthenticationProvider> locateRequestedProvider(final Collection<MultifactorAuthenticationProvider> providersArray, final String requestedProvider) { return providersArray.stream() .filter(provider -> provider.getId().equals(requestedProvider)) .findFirst(); } private RegisteredServiceMultifactorPolicy.FailureModes getMultifactorFailureModeForService(final RegisteredService service) { final RegisteredServiceMultifactorPolicy policy = service.getMultifactorPolicy(); if (policy == null || policy.getFailureMode() == null) { return RegisteredServiceMultifactorPolicy.FailureModes.valueOf(this.globalFailureMode); } return policy.getFailureMode(); } }