package org.apereo.cas.authentication;
import com.codahale.metrics.annotation.Counted;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.Timed;
import org.apereo.cas.authentication.exceptions.UnresolvedPrincipalException;
import org.apereo.cas.authentication.principal.NullPrincipal;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.support.events.authentication.CasAuthenticationPrincipalResolvedEvent;
import org.apereo.cas.support.events.authentication.CasAuthenticationTransactionStartedEvent;
import org.apereo.cas.support.events.authentication.CasAuthenticationTransactionSuccessfulEvent;
import org.apereo.inspektr.audit.annotation.Audit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.util.Assert;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Set;
/**
* This is {@link AbstractAuthenticationManager}, which provides common operations
* around an authentication manager implementation.
*
* @author Misagh Moayyed
* @since 5.0.0
*/
public abstract class AbstractAuthenticationManager implements AuthenticationManager {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAuthenticationManager.class);
/**
* Plan to execute the authentication transaction.
*/
protected final AuthenticationEventExecutionPlan authenticationEventExecutionPlan;
/**
* The Authentication handler resolver.
*/
protected final AuthenticationHandlerResolver authenticationHandlerResolver;
/**
* Indicate if principal resolution should totally fail
* and no fall back onto principal that is produced by the
* authentication handler.
*/
protected boolean principalResolutionFailureFatal;
@Autowired
private ApplicationEventPublisher eventPublisher;
/**
* Creates a new authentication manager with a map of authentication handlers to the principal resolvers that
* should be used upon successful authentication if no principal is resolved by the authentication handler. If
* the order of evaluation of authentication handlers is important, a map that preserves insertion order
* (e.g. {@link LinkedHashMap}) should be used.
*
* @param authenticationEventExecutionPlan Describe the execution plan for this manager
* @param authenticationHandlerResolver the authentication handler resolver
* @param principalResolutionFatal the principal resolution fatal
*/
protected AbstractAuthenticationManager(final AuthenticationEventExecutionPlan authenticationEventExecutionPlan,
final AuthenticationHandlerResolver authenticationHandlerResolver,
final boolean principalResolutionFatal) {
Assert.notNull(authenticationEventExecutionPlan);
Assert.notNull(authenticationHandlerResolver);
Assert.notNull(principalResolutionFatal);
this.authenticationEventExecutionPlan = authenticationEventExecutionPlan;
this.authenticationHandlerResolver = authenticationHandlerResolver;
this.principalResolutionFailureFatal = principalResolutionFatal;
}
/**
* Populate authentication metadata attributes.
*
* @param builder the builder
* @param transaction the transaction
*/
protected void populateAuthenticationMetadataAttributes(final AuthenticationBuilder builder,
final AuthenticationTransaction transaction) {
LOGGER.debug("Invoking authentication metadata populators for authentication transaction");
final Collection<AuthenticationMetaDataPopulator> pops = getAuthenticationMetadataPopulatorsForTransaction(transaction);
pops.forEach(populator -> transaction.getCredentials().stream().filter(populator::supports)
.forEach(credential -> populator.populateAttributes(builder, transaction)));
}
/**
* Add authentication method attribute.
*
* @param builder the builder
* @param authentication the authentication
*/
protected void addAuthenticationMethodAttribute(final AuthenticationBuilder builder,
final Authentication authentication) {
authentication.getSuccesses().values().forEach(result -> builder.addAttribute(AUTHENTICATION_METHOD_ATTRIBUTE, result.getHandlerName()));
}
/**
* Resolve principal.
*
* @param handler the handler name
* @param resolver the resolver
* @param credential the credential
* @param principal the current authenticated principal from a handler, if any.
* @return the principal
*/
protected Principal resolvePrincipal(final AuthenticationHandler handler, final PrincipalResolver resolver,
final Credential credential, final Principal principal) {
if (resolver.supports(credential)) {
try {
final Principal p = resolver.resolve(credential, principal, handler);
LOGGER.debug("[{}] resolved [{}] from [{}]", resolver, p, credential);
return p;
} catch (final Exception e) {
LOGGER.error("[{}] failed to resolve principal from [{}]", resolver, credential, e);
}
} else {
LOGGER.warn(
"[{}] is configured to use [{}] but it does not support [{}], which suggests a configuration problem.",
handler.getName(), resolver, credential);
}
return null;
}
@Override
@Audit(
action = "AUTHENTICATION",
actionResolverName = "AUTHENTICATION_RESOLVER",
resourceResolverName = "AUTHENTICATION_RESOURCE_RESOLVER")
@Timed(name = "AUTHENTICATE_TIMER")
@Metered(name = "AUTHENTICATE_METER")
@Counted(name = "AUTHENTICATE_COUNT", monotonic = true)
public Authentication authenticate(final AuthenticationTransaction transaction) throws AuthenticationException {
AuthenticationCredentialsLocalBinder.bindCurrent(transaction.getCredentials());
final AuthenticationBuilder builder = authenticateInternal(transaction);
final Authentication authentication = builder.build();
final Principal principal = authentication.getPrincipal();
if (principal instanceof NullPrincipal) {
throw new UnresolvedPrincipalException(authentication);
}
addAuthenticationMethodAttribute(builder, authentication);
LOGGER.info("Authenticated principal [{}] with attributes [{}] via credentials [{}].",
principal.getId(), principal.getAttributes(), transaction.getCredentials());
populateAuthenticationMetadataAttributes(builder, transaction);
final Authentication a = builder.build();
AuthenticationCredentialsLocalBinder.bindCurrent(a);
return a;
}
/**
* Authenticate and resolve principal.
*
* @param builder the builder
* @param credential the credential
* @param resolver the resolver
* @param handler the handler
* @throws GeneralSecurityException the general security exception
* @throws PreventedException the prevented exception
*/
protected void authenticateAndResolvePrincipal(final AuthenticationBuilder builder,
final Credential credential,
final PrincipalResolver resolver,
final AuthenticationHandler handler) throws GeneralSecurityException, PreventedException {
Principal principal;
publishEvent(new CasAuthenticationTransactionStartedEvent(this, credential));
final HandlerResult result = handler.authenticate(credential);
builder.addSuccess(handler.getName(), result);
LOGGER.debug("Authentication handler [{}] successfully authenticated [{}]", handler.getName(), credential);
publishEvent(new CasAuthenticationTransactionSuccessfulEvent(this, credential));
principal = result.getPrincipal();
if (resolver == null) {
LOGGER.debug("No principal resolution is configured for [{}]. Falling back to handler principal [{}]",
handler.getName(),
principal);
} else {
principal = resolvePrincipal(handler, resolver, credential, principal);
if (principal == null) {
if (this.principalResolutionFailureFatal) {
LOGGER.warn("Principal resolution handled by [{}] produced a null principal for: [{}]"
+ "CAS is configured to treat principal resolution failures as fatal.",
resolver.getClass().getSimpleName(), credential);
throw new UnresolvedPrincipalException();
}
LOGGER.warn("Principal resolution handled by [{}] produced a null principal. "
+ "This is likely due to misconfiguration or missing attributes; CAS will attempt to use the principal "
+ "produced by the authentication handler, if any.", resolver.getClass().getSimpleName());
}
}
if (principal != null) {
builder.setPrincipal(principal);
}
LOGGER.debug("Final principal resolved for this authentication event is [{}]", principal);
publishEvent(new CasAuthenticationPrincipalResolvedEvent(this, principal));
}
/**
* Follows the same contract as {@link AuthenticationManager#authenticate(AuthenticationTransaction)}.
*
* @param transaction the authentication transaction
* @return An authentication containing a resolved principal and metadata about successful and failed authentications.
* There SHOULD be a record of each attempted authentication, whether success or failure.
* @throws AuthenticationException When one or more credentials failed authentication such that security policy was not satisfied.
*/
protected abstract AuthenticationBuilder authenticateInternal(AuthenticationTransaction transaction) throws AuthenticationException;
/**
* Gets authentication handlers for this transaction.
*
* @param transaction the transaction
* @return the authentication handlers for this transaction
*/
protected Set<AuthenticationHandler> getAuthenticationHandlersForThisTransaction(final AuthenticationTransaction transaction) {
final Set<AuthenticationHandler> handlers = this.authenticationEventExecutionPlan.getAuthenticationHandlersForTransaction(transaction);
return this.authenticationHandlerResolver.resolve(handlers, transaction);
}
/**
* Gets principal resolver linked to the handler if any.
*
* @param handler the handler
* @param transaction the transaction
* @return the principal resolver linked to handler if any, or null.
*/
protected PrincipalResolver getPrincipalResolverLinkedToHandlerIfAny(final AuthenticationHandler handler, final AuthenticationTransaction transaction) {
return this.authenticationEventExecutionPlan.getPrincipalResolverForAuthenticationTransaction(handler, transaction);
}
/**
* Gets authentication metadata populators for transaction.
*
* @param transaction the transaction
* @return the authentication metadata populators for transaction
*/
protected Collection<AuthenticationMetaDataPopulator> getAuthenticationMetadataPopulatorsForTransaction(
final AuthenticationTransaction transaction) {
return this.authenticationEventExecutionPlan.getAuthenticationMetadataPopulators(transaction);
}
/**
* Publish event.
*
* @param event the event
*/
protected void publishEvent(final ApplicationEvent event) {
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(event);
}
}
}