package org.apereo.cas.web.flow.resolver.impl; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.AuthenticationException; import org.apereo.cas.authentication.AuthenticationServiceSelectionPlan; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.adaptive.geo.GeoLocationRequest; import org.apereo.cas.authentication.adaptive.geo.GeoLocationResponse; import org.apereo.cas.authentication.adaptive.geo.GeoLocationService; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.services.MultifactorAuthenticationProvider; import org.apereo.cas.services.MultifactorAuthenticationProviderSelector; import org.apereo.cas.services.RegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.ticket.registry.TicketRegistrySupport; import org.apereo.cas.web.flow.authentication.BaseMultifactorAuthenticationProviderEventResolver; import org.apereo.cas.web.support.WebUtils; import org.apereo.inspektr.audit.annotation.Audit; import org.apereo.inspektr.common.web.ClientInfo; import org.apereo.inspektr.common.web.ClientInfoHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.util.CookieGenerator; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; /** * This is {@link AdaptiveMultifactorAuthenticationPolicyEventResolver}, * which handles the initial authentication attempt and calls upon a number of * embedded resolvers to produce the next event in the authentication flow. * * @author Misagh Moayyed * @since 5.0.0 */ public class AdaptiveMultifactorAuthenticationPolicyEventResolver extends BaseMultifactorAuthenticationProviderEventResolver { private static final Logger LOGGER = LoggerFactory.getLogger(AdaptiveMultifactorAuthenticationPolicyEventResolver.class); private final GeoLocationService geoLocationService; private final Map multifactorMap; public AdaptiveMultifactorAuthenticationPolicyEventResolver(final AuthenticationSystemSupport authenticationSystemSupport, final CentralAuthenticationService centralAuthenticationService, final ServicesManager servicesManager, final TicketRegistrySupport ticketRegistrySupport, final CookieGenerator warnCookieGenerator, final AuthenticationServiceSelectionPlan authenticationSelectionStrategies, final MultifactorAuthenticationProviderSelector selector, final CasConfigurationProperties casProperties, final GeoLocationService geoLocationService) { super(authenticationSystemSupport, centralAuthenticationService, servicesManager, ticketRegistrySupport, warnCookieGenerator, authenticationSelectionStrategies, selector); multifactorMap = casProperties.getAuthn().getAdaptive().getRequireMultifactor(); this.geoLocationService = geoLocationService; } @Override public Set<Event> resolveInternal(final RequestContext context) { final RegisteredService service = resolveRegisteredServiceInRequestContext(context); final Authentication authentication = WebUtils.getAuthentication(context); if (service == null || authentication == null) { LOGGER.debug("No service or authentication is available to determine event for principal"); return null; } if (multifactorMap == null || multifactorMap.isEmpty()) { LOGGER.debug("Adaptive authentication is not configured to require multifactor authentication"); return null; } final Map<String, MultifactorAuthenticationProvider> providerMap = WebUtils.getAvailableMultifactorAuthenticationProviders(this.applicationContext); if (providerMap == null || providerMap.isEmpty()) { LOGGER.error("No multifactor authentication providers are available in the application context"); throw new AuthenticationException(); } final Set<Event> providerFound = checkRequireMultifactorProvidersForRequest(context, service, authentication); if (providerFound != null && !providerFound.isEmpty()) { LOGGER.warn("Found multifactor authentication providers [{}] required for this authentication event", providerFound); return providerFound; } return null; } private Set<Event> checkRequireMultifactorProvidersForRequest(final RequestContext context, final RegisteredService service, final Authentication authentication) { final ClientInfo clientInfo = ClientInfoHolder.getClientInfo(); final String clientIp = clientInfo.getClientIpAddress(); LOGGER.debug("Located client IP address as [{}]", clientIp); final String agent = WebUtils.getHttpServletRequestUserAgent(); final Map<String, MultifactorAuthenticationProvider> providerMap = WebUtils.getAvailableMultifactorAuthenticationProviders(this.applicationContext); final Set<Map.Entry> entries = multifactorMap.entrySet(); for (final Map.Entry entry : entries) { final String mfaMethod = entry.getKey().toString(); final String pattern = entry.getValue().toString(); final Optional<MultifactorAuthenticationProvider> providerFound = resolveProvider(providerMap, mfaMethod); if (!providerFound.isPresent()) { LOGGER.error("Adaptive authentication is configured to require [{}] for [{}], yet [{}] is absent in the configuration.", mfaMethod, pattern, mfaMethod); throw new AuthenticationException(); } if (checkUserAgentOrClientIp(clientIp, agent, mfaMethod, pattern)) { return buildEvent(context, service, authentication, providerFound.get()); } if (checkRequestGeoLocation(clientIp, mfaMethod, pattern)) { return buildEvent(context, service, authentication, providerFound.get()); } } return null; } private boolean checkRequestGeoLocation(final String clientIp, final String mfaMethod, final String pattern) { if (this.geoLocationService != null) { final GeoLocationRequest location = WebUtils.getHttpServletRequestGeoLocation(); final GeoLocationResponse loc = this.geoLocationService.locate(clientIp, location); if (loc != null) { final String address = loc.build(); if (address.matches(pattern)) { LOGGER.debug("Current address [{}] at [{}] matches the provided pattern [{}] for " + "adaptive authentication and is required to use [{}]", address, clientIp, pattern, mfaMethod); return true; } } } return false; } private static boolean checkUserAgentOrClientIp(final String clientIp, final String agent, final String mfaMethod, final String pattern) { if (agent.matches(pattern) || clientIp.matches(pattern)) { LOGGER.debug("Current user agent [{}] at [{}] matches the provided pattern [{}] for " + "adaptive authentication and is required to use [{}]", agent, clientIp, pattern, mfaMethod); return true; } return false; } private Set<Event> buildEvent(final RequestContext context, final RegisteredService service, final Authentication authentication, final MultifactorAuthenticationProvider provider) { if (provider.isAvailable(service)) { LOGGER.debug("Attempting to build an event based on the authentication provider [{}] and service [{}]", provider, service.getName()); final Event event = validateEventIdForMatchingTransitionInContext(provider.getId(), context, buildEventAttributeMap(authentication.getPrincipal(), service, provider)); return Collections.singleton(event); } LOGGER.warn("Located multifactor provider [{}], yet the provider cannot be reached or verified", provider); return null; } @Audit(action = "AUTHENTICATION_EVENT", actionResolverName = "AUTHENTICATION_EVENT_ACTION_RESOLVER", resourceResolverName = "AUTHENTICATION_EVENT_RESOURCE_RESOLVER") @Override public Event resolveSingle(final RequestContext context) { return super.resolveSingle(context); } }