/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at the following location:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jasig.cas.authentication;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotNull;
import com.github.inspektr.audit.annotation.Audit;
import org.jasig.cas.authentication.principal.PrincipalResolver;
import org.jasig.cas.authentication.principal.Principal;
import org.perf4j.aop.Profiled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
/**
* Provides an authenticaiton 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:</li>
* <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:</li>
* <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>Check whether the security policy (e.g. any, all) is satisfied.</li>
* <ul>
* <li>If security policy is met return immediately.</li>
* <li>Continue if security policy is not met.</li>
* </ul>
* </ul>
* <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 {@link #setAuthenticationPolicy(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
*/
public class PolicyBasedAuthenticationManager implements AuthenticationManager {
/** Default principal implementation that allows us to create {@link Authentication}s (principal cannot be null). */
private static final Principal NULL_PRINCIPAL = new NullPrincipal();
/** Log instance for logging events, errors, warnings, etc. */
protected final Logger logger = LoggerFactory.getLogger(getClass());
/** An array of AuthenticationAttributesPopulators. */
@NotNull
private List<AuthenticationMetaDataPopulator> authenticationMetaDataPopulators =
new ArrayList<AuthenticationMetaDataPopulator>();
/** Authentication security policy. */
@NotNull
private AuthenticationPolicy authenticationPolicy = new AnyAuthenticationPolicy();
/** Map of authentication handlers to resolvers to be used when handler does not resolve a principal. */
@NotNull
private final Map<AuthenticationHandler, PrincipalResolver> handlerResolverMap;
/**
* Creates a new authentication manager with a varargs array of authentication handlers that are attempted in the
* listed order for supported credentials. This form may only be used by authentication handlers that
* resolve principals during the authentication process.
*
* @param handlers One or more authentication handlers.
*/
public PolicyBasedAuthenticationManager(final AuthenticationHandler ... handlers) {
this(Arrays.asList(handlers));
}
/**
* Creates a new authentication manager with a list of authentication handlers that are attempted in the
* listed order for supported credentials. This form may only be used by authentication handlers that
* resolve principals during the authentication process.
*
* @param handlers Non-null list of authentication handlers containing at least one entry.
*/
public PolicyBasedAuthenticationManager(final List<AuthenticationHandler> handlers) {
Assert.notEmpty(handlers, "At least one authentication handler is required");
this.handlerResolverMap = new LinkedHashMap<AuthenticationHandler, PrincipalResolver>(
handlers.size());
for (final AuthenticationHandler handler : handlers) {
this.handlerResolverMap.put(handler, null);
}
}
/**
* 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 map Non-null map of authentication handler to principal resolver containing at least one entry.
*/
public PolicyBasedAuthenticationManager(final Map<AuthenticationHandler, PrincipalResolver> map) {
Assert.notEmpty(map, "At least one authentication handler is required");
this.handlerResolverMap = map;
}
/** {@inheritDoc} */
@Override
@Audit(
action="AUTHENTICATION",
actionResolverName="AUTHENTICATION_RESOLVER",
resourceResolverName="AUTHENTICATION_RESOURCE_RESOLVER")
@Profiled(tag = "AUTHENTICATE", logFailuresSeparately = false)
public final Authentication authenticate(final Credential... credentials) throws AuthenticationException {
final AuthenticationBuilder builder = authenticateInternal(credentials);
final Authentication authentication = builder.build();
final Principal principal = authentication.getPrincipal();
if (principal instanceof NullPrincipal) {
throw new UnresolvedPrincipalException(authentication);
}
for (final HandlerResult result : authentication.getSuccesses().values()) {
builder.addAttribute(AUTHENTICATION_METHOD_ATTRIBUTE, result.getHandlerName());
}
logger.info("Authenticated {} with credentials {}.", principal, Arrays.asList(credentials));
logger.debug("Attribute map for {}: {}", principal.getId(), principal.getAttributes());
for (final AuthenticationMetaDataPopulator populator : this.authenticationMetaDataPopulators) {
for (final Credential credential : credentials) {
populator.populateAttributes(builder, credential);
}
}
return builder.build();
}
/**
* Sets the authentication metadata populators that will be applied to every successful authentication event.
*
* @param populators Non-null list of metadata populators.
*/
public final void setAuthenticationMetaDataPopulators(final List<AuthenticationMetaDataPopulator> populators) {
this.authenticationMetaDataPopulators = populators;
}
/**
* Sets the authentication policy used by this component.
*
* @param policy Non-null authentication policy. The default policy is {@link AnyAuthenticationPolicy}.
*/
public void setAuthenticationPolicy(final AuthenticationPolicy policy) {
this.authenticationPolicy = policy;
}
/**
* Follows the same contract as {@link AuthenticationManager#authenticate(Credential...)}.
*
* @param credentials One or more credentials to authenticate.
*
* @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 AuthenticationBuilder authenticateInternal(final Credential... credentials)
throws AuthenticationException {
final AuthenticationBuilder builder = new AuthenticationBuilder(NULL_PRINCIPAL);
for (final Credential c : credentials) {
builder.addCredential(new BasicCredentialMetaData(c));
}
boolean found;
Principal principal;
PrincipalResolver resolver;
for (final Credential credential : credentials) {
found = false;
for (final AuthenticationHandler handler : this.handlerResolverMap.keySet()) {
if (handler.supports(credential)) {
found = true;
try {
final HandlerResult result = handler.authenticate(credential);
builder.addSuccess(handler.getName(), result);
logger.info("{} successfully authenticated {}", handler.getName(), credential);
resolver = this.handlerResolverMap.get(handler);
if (resolver == null) {
principal = result.getPrincipal();
logger.debug(
"No resolver configured for {}. Falling back to handler principal {}",
handler.getName(),
principal);
} else {
principal = resolvePrincipal(handler.getName(), resolver, credential);
}
// Must avoid null principal since AuthenticationBuilder/ImmutableAuthentication
// require principal to be non-null
if (principal != null) {
builder.setPrincipal(principal);
}
if (this.authenticationPolicy.isSatisfiedBy(builder.build())) {
return builder;
}
} catch (final GeneralSecurityException e) {
logger.info("{} failed authenticating {}", handler.getName(), credential);
builder.addFailure(handler.getName(), e.getClass());
} catch (final PreventedException e) {
builder.addFailure(handler.getName(), e.getClass());
}
}
}
if (!found) {
logger.warn(
"Cannot find authentication handler that supports {}, which suggests a configuration problem.",
credential);
}
}
// We apply an implicit security policy of at least one successful authentication
if (builder.getSuccesses().isEmpty()) {
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
// Apply the configured security policy
if (!this.authenticationPolicy.isSatisfiedBy(builder.build())) {
throw new AuthenticationException(builder.getFailures(), builder.getSuccesses());
}
return builder;
}
protected Principal resolvePrincipal(
final String handlerName, final PrincipalResolver resolver, final Credential credential) {
if (resolver.supports(credential)) {
try {
final Principal p = resolver.resolve(credential);
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.",
handlerName,
resolver,
credential);
}
return null;
}
/**
* Creates a new authentication exception from an authentication event.
*
* @param authn Authentication event.
*
* @return Authentication exception containing information about authentication successes and failures.
*/
private static AuthenticationException createAuthenticationException(final Authentication authn) {
return new AuthenticationException(authn.getFailures(), authn.getSuccesses());
}
/**
* Null prinicpal implementation that allows us to construct {@link Authentication}s in the event that no
* principal is resolved during the authentication process.
*/
static class NullPrincipal implements Principal {
/** The nobody principal. */
private static final String NOBODY = "nobody";
@Override
public String getId() {
return NOBODY;
}
@Override
public Map<String, Object> getAttributes() {
return Collections.emptyMap();
}
}
}