package org.apereo.cas.services; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.AuthenticationHandler; import org.apereo.cas.authentication.AuthenticationManager; import org.apereo.cas.authentication.DefaultAuthenticationBuilder; import org.apereo.cas.authentication.principal.Principal; import org.apereo.cas.configuration.model.support.mfa.MultifactorAuthenticationProperties; import org.apereo.cas.util.CollectionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * This is {@link DefaultMultifactorAuthenticationProviderBypass}. * * @author Misagh Moayyed * @since 5.0.0 */ public class DefaultMultifactorAuthenticationProviderBypass implements MultifactorAuthenticationProviderBypass { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMultifactorAuthenticationProviderBypass.class); private static final long serialVersionUID = 3720922341350004543L; private final MultifactorAuthenticationProperties.BaseProvider.Bypass bypass; public DefaultMultifactorAuthenticationProviderBypass(final MultifactorAuthenticationProperties.BaseProvider.Bypass bypass) { this.bypass = bypass; } @Override public boolean isAuthenticationRequestHonored(final Authentication authentication, final RegisteredService registeredService, final MultifactorAuthenticationProvider provider) { final Principal principal = authentication.getPrincipal(); final boolean bypassByPrincipal = locateMatchingAttributeBasedOnPrincipalAttributes(bypass, principal); if (bypassByPrincipal) { LOGGER.debug("Bypass rules for principal [{}] indicate the request may be ignored", principal.getId()); updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } final boolean bypassByAuthn = locateMatchingAttributeBasedOnAuthenticationAttributes(bypass, authentication); if (bypassByAuthn) { LOGGER.debug("Bypass rules for authentication [{}] indicate the request may be ignored", principal.getId()); updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } final boolean bypassByAuthnMethod = locateMatchingAttributeValue( AuthenticationManager.AUTHENTICATION_METHOD_ATTRIBUTE, bypass.getAuthenticationMethodName(), authentication.getAttributes(), false ); if (bypassByAuthnMethod) { LOGGER.debug("Bypass rules for authentication method [{}] indicate the request may be ignored", principal.getId()); updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } final boolean bypassByHandlerName = locateMatchingAttributeValue( AuthenticationHandler.SUCCESSFUL_AUTHENTICATION_HANDLERS, bypass.getAuthenticationHandlerName(), authentication.getAttributes(), false ); if (bypassByHandlerName) { LOGGER.debug("Bypass rules for authentication handlers [{}] indicate the request may be ignored", principal.getId()); updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } final boolean bypassByCredType = locateMatchingCredentialType(authentication, bypass.getCredentialClassType()); if (bypassByCredType) { LOGGER.debug("Bypass rules for credential types [{}] indicate the request may be ignored", principal.getId()); updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } final boolean bypassByService = locateMatchingRegisteredServiceForBypass(authentication, registeredService); if (bypassByService) { updateAuthenticationToRememberBypass(authentication, provider, principal); return false; } updateAuthenticationToForgetBypass(authentication, provider, principal); return true; } private static void updateAuthenticationToForgetBypass(final Authentication authentication, final MultifactorAuthenticationProvider provider, final Principal principal) { LOGGER.debug("Bypass rules for service [{}] indicate the request may be ignored", principal.getId()); final Authentication newAuthn = DefaultAuthenticationBuilder.newInstance(authentication) .addAttribute(AUTHENTICATION_ATTRIBUTE_BYPASS_MFA, Boolean.FALSE) .build(); LOGGER.debug("Updated authentication session to remember bypass for [{}] via [{}]", provider.getId(), AUTHENTICATION_ATTRIBUTE_BYPASS_MFA); authentication.updateAll(newAuthn); } private static void updateAuthenticationToRememberBypass(final Authentication authentication, final MultifactorAuthenticationProvider provider, final Principal principal) { LOGGER.debug("Bypass rules for service [{}] indicate the request may NOT be ignored", principal.getId()); final Authentication newAuthn = DefaultAuthenticationBuilder.newInstance(authentication) .addAttribute(AUTHENTICATION_ATTRIBUTE_BYPASS_MFA, Boolean.TRUE) .addAttribute(AUTHENTICATION_ATTRIBUTE_BYPASS_MFA_PROVIDER, provider.getId()) .build(); LOGGER.debug("Updated authentication session to NOT remember bypass for [{}] via [{}]", provider.getId(), AUTHENTICATION_ATTRIBUTE_BYPASS_MFA); authentication.updateAll(newAuthn); } /** * Locate matching registered service property boolean. * * @param authentication the authentication * @param registeredService the registered service * @return true/false */ protected boolean locateMatchingRegisteredServiceForBypass(final Authentication authentication, final RegisteredService registeredService) { if (registeredService != null && registeredService.getMultifactorPolicy() != null) { return registeredService.getMultifactorPolicy().isBypassEnabled(); } return false; } /** * Locate matching credential type boolean. * * @param authentication the authentication * @param credentialClassType the credential class type * @return the boolean */ protected boolean locateMatchingCredentialType(final Authentication authentication, final String credentialClassType) { return StringUtils.isNotBlank(credentialClassType) && authentication.getCredentials() .stream() .filter(e -> e.getCredentialClass().getName().matches(credentialClassType)) .findAny() .isPresent(); } /** * Skip bypass and support event based on authentication attributes. * * @param bypass the bypass * @param authn the authn * @return the boolean */ protected boolean locateMatchingAttributeBasedOnAuthenticationAttributes( final MultifactorAuthenticationProperties.BaseProvider.Bypass bypass, final Authentication authn) { return locateMatchingAttributeValue(bypass.getAuthenticationAttributeName(), bypass.getAuthenticationAttributeValue(), authn.getAttributes()); } /** * Skip bypass and support event based on principal attributes. * * @param bypass the bypass * @param principal the principal * @return the boolean */ protected boolean locateMatchingAttributeBasedOnPrincipalAttributes( final MultifactorAuthenticationProperties.BaseProvider.Bypass bypass, final Principal principal) { return locateMatchingAttributeValue(bypass.getPrincipalAttributeName(), bypass.getAuthenticationAttributeValue(), principal.getAttributes()); } /** * Locate matching attribute value boolean. * * @param attrName the attr name * @param attrValue the attr value * @param attributes the attributes * @return true/false */ protected boolean locateMatchingAttributeValue(final String attrName, final String attrValue, final Map<String, Object> attributes) { return locateMatchingAttributeValue(attrName, attrValue, attributes, true); } /** * Evaluate attribute rules for bypass. * * @param attrName the attr name * @param attrValue the attr value * @param attributes the attributes * @param matchIfNoValueProvided the force match on value * @return true a matching attribute name/value is found */ protected boolean locateMatchingAttributeValue(final String attrName, final String attrValue, final Map<String, Object> attributes, final boolean matchIfNoValueProvided) { LOGGER.debug("Locating matching attribute [{}] with value [{}] amongst the attribute collection [{}]", attrName, attrValue, attributes); if (StringUtils.isBlank(attrName)) { LOGGER.debug("Failed to match since attribute name is undefined"); return false; } final Set<Map.Entry<String, Object>> names = attributes.entrySet() .stream() .filter(e -> { LOGGER.debug("Attempting to match [{}] against [{}]", attrName, e.getKey()); return e.getKey().matches(attrName); } ).collect(Collectors.toSet()); LOGGER.debug("Found [{}] attributes relevant for multifactor authentication bypass", names.size()); if (names.isEmpty()) { return false; } if (StringUtils.isBlank(attrValue)) { LOGGER.debug("No attribute value to match is provided; Match result is set to [{}]", matchIfNoValueProvided); return matchIfNoValueProvided; } final Set<Map.Entry<String, Object>> values = names .stream() .filter(e -> { final Set<Object> valuesCol = CollectionUtils.toCollection(e.getValue()); LOGGER.debug("Matching attribute [{}] with values [{}] against [{}]", e.getKey(), valuesCol, attrValue); return valuesCol.stream() .filter(v -> v.toString().matches(attrValue)) .findAny() .isPresent(); }).collect(Collectors.toSet()); LOGGER.debug("Matching attribute values remaining are [{}]", values); return !values.isEmpty(); } }