package org.apereo.cas.authentication;
import org.apereo.cas.authentication.policy.AnyAuthenticationPolicy;
import org.apereo.cas.authentication.principal.NullPrincipal;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.support.events.authentication.CasAuthenticationPolicyFailureEvent;
import org.apereo.cas.support.events.authentication.CasAuthenticationTransactionFailureEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.OrderComparator;
import org.springframework.util.Assert;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* Provides an authentication manager that is inherently aware of multiple credentials and supports pluggable
* security policy via the {@link AuthenticationPolicy} component. The authentication process is as follows:
* <ul>
* <li>For each given credential do the following:
* <ul>
* <li>Iterate over all configured authentication handlers.</li>
* <li>Attempt to authenticate a credential if a handler supports it.</li>
* <li>On success attempt to resolve a principal by doing the following:
* <ul>
* <li>Check whether a resolver is configured for the handler that authenticated the credential.</li>
* <li>If a suitable resolver is found, attempt to resolve the principal.</li>
* <li>If a suitable resolver is not found, use the principal resolved by the authentication handler.</li>
* </ul>
* </li>
* <li>Check whether the security policy (e.g. any, all) is satisfied.
* <ul>
* <li>If security policy is met return immediately.</li>
* <li>Continue if security policy is not met.</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>
* After all credentials have been attempted check security policy again.
* Note there is an implicit security policy that requires at least one credential to be authenticated.
* Then the security policy given by the {@link AuthenticationPolicy} is applied.
* In all cases {@link AuthenticationException} is raised if security policy is not met.
* </li>
* </ul>
* It is an error condition to fail to resolve a principal.
*
* @author Marvin S. Addison
* @since 4.0.0
*/
public class PolicyBasedAuthenticationManager extends AbstractAuthenticationManager {
private static final Logger LOGGER = LoggerFactory.getLogger(PolicyBasedAuthenticationManager.class);
/**
* Authentication security policy.
*/
protected final Collection<AuthenticationPolicy> authenticationPolicies;
/**
* Instantiates a new Policy based authentication manager.
*
* @param authenticationEventExecutionPlan the execution plan
* @param authenticationHandlerResolver the authentication handler resolver
* @param authenticationPolicies the authentication policy
* @param principalResolutionFatal the principal resolution fatal
*/
public PolicyBasedAuthenticationManager(final AuthenticationEventExecutionPlan authenticationEventExecutionPlan,
final AuthenticationHandlerResolver authenticationHandlerResolver,
final Collection<AuthenticationPolicy> authenticationPolicies,
final boolean principalResolutionFatal) {
super(authenticationEventExecutionPlan, authenticationHandlerResolver, principalResolutionFatal);
this.authenticationPolicies = authenticationPolicies;
}
/**
* Instantiates a new Policy based authentication manager.
*
* @param authenticationEventExecutionPlan the authentication event execution plan
* @param servicesManager the services manager
*/
public PolicyBasedAuthenticationManager(final AuthenticationEventExecutionPlan authenticationEventExecutionPlan,
final ServicesManager servicesManager) {
this(authenticationEventExecutionPlan, servicesManager, Arrays.asList(new AnyAuthenticationPolicy(false)));
}
/**
* Instantiates a new Policy based authentication manager.
*
* @param authenticationEventExecutionPlan the authentication event execution plan
* @param servicesManager the services manager
* @param authenticationPolicy the authentication policy
*/
public PolicyBasedAuthenticationManager(final AuthenticationEventExecutionPlan authenticationEventExecutionPlan,
final ServicesManager servicesManager,
final Collection<AuthenticationPolicy> authenticationPolicy) {
super(authenticationEventExecutionPlan, new RegisteredServiceAuthenticationHandlerResolver(servicesManager), false);
this.authenticationPolicies = authenticationPolicy;
}
public PolicyBasedAuthenticationManager(final AuthenticationEventExecutionPlan authenticationEventExecutionPlan,
final ServicesManager servicesManager,
final AuthenticationPolicy authenticationPolicy) {
super(authenticationEventExecutionPlan, new RegisteredServiceAuthenticationHandlerResolver(servicesManager), false);
this.authenticationPolicies = Arrays.asList(authenticationPolicy);
}
@Override
protected AuthenticationBuilder authenticateInternal(final AuthenticationTransaction transaction) throws AuthenticationException {
final Collection<Credential> credentials = transaction.getCredentials();
final AuthenticationBuilder builder = new DefaultAuthenticationBuilder(NullPrincipal.getInstance());
credentials.stream().forEach(cred -> builder.addCredential(new BasicCredentialMetaData(cred)));
final Set<AuthenticationHandler> handlerSet = getAuthenticationHandlersForThisTransaction(transaction);
Assert.notNull(handlerSet, "Resolved authentication handlers for this transaction cannot be null");
if (handlerSet.isEmpty()) {
LOGGER.warn("Resolved authentication handlers for this transaction are empty");
}
final boolean success = credentials.stream().anyMatch(credential -> {
final boolean isSatisfied = handlerSet.stream().filter(handler -> handler.supports(credential))
.anyMatch(handler -> {
try {
final PrincipalResolver resolver = getPrincipalResolverLinkedToHandlerIfAny(handler, transaction);
authenticateAndResolvePrincipal(builder, credential, resolver, handler);
return this.authenticationPolicies.stream().allMatch(p -> p.isSatisfiedBy(builder.build()));
} catch (final GeneralSecurityException e) {
LOGGER.info("[{}] failed authenticating [{}]", handler.getName(), credential);
LOGGER.debug("[{}] exception details: [{}]", handler.getName(), e.getMessage());
builder.addFailure(handler.getName(), e.getClass());
} catch (final PreventedException e) {
LOGGER.error("[{}]: [{}] (Details: [{}])", handler.getName(), e.getMessage(), e.getCause().getMessage());
builder.addFailure(handler.getName(), e.getClass());
}
return false;
});
if (isSatisfied) {
return true;
}
LOGGER.warn("Authentication has failed. Credentials may be incorrect or CAS cannot find authentication handler that "
+ "supports [{}] of type [{}], which suggests a configuration problem.",
credential, credential.getClass().getSimpleName());
return false;
});
if (!success) {
evaluateProducedAuthenticationContext(builder, transaction);
}
return builder;
}
/**
* Evaluate produced authentication context.
* We apply an implicit security policy of at least one successful authentication.
* Then, we apply the configured security policy.
*
* @param builder the builder
* @param transaction the transaction
* @throws AuthenticationException the authentication exception
*/
protected void evaluateProducedAuthenticationContext(final AuthenticationBuilder builder,
final AuthenticationTransaction transaction) throws AuthenticationException {
if (builder.getSuccesses().isEmpty()) {
publishEvent(new CasAuthenticationTransactionFailureEvent(this, builder.getFailures(), transaction.getCredentials()));
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
final Authentication authentication = builder.build();
final List<AuthenticationPolicy> policies = new ArrayList<>(this.authenticationPolicies);
OrderComparator.sort(policies);
final boolean result = policies.stream().allMatch(p -> {
LOGGER.debug("Executing authentication policy [{}]", p.getClass().getSimpleName());
return p.isSatisfiedBy(authentication);
});
if (!result) {
publishEvent(new CasAuthenticationPolicyFailureEvent(this, builder.getFailures(), transaction, authentication));
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
}
}