/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed 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
*
* 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.springframework.security.authentication;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;
/**
* Iterates an {@link Authentication} request through a list of {@link AuthenticationProvider}s.
*
* <p>
* <tt>AuthenticationProvider</tt>s are usually tried in order until one provides a non-null response.
* A non-null response indicates the provider had authority to decide on the authentication request and no further
* providers are tried.
* If a subsequent provider successfully authenticates the request, the earlier authentication exception is disregarded
* and the successful authentication will be used. If no subsequent provider provides a non-null response, or a new
* <code>AuthenticationException</code>, the last <code>AuthenticationException</code> received will be used.
* If no provider returns a non-null response, or indicates it can even process an <code>Authentication</code>,
* the <code>ProviderManager</code> will throw a <code>ProviderNotFoundException</code>.
* A parent {@code AuthenticationManager} can also be set, and this will also be tried if none of the configured
* providers can perform the authentication. This is intended to support namespace configuration options though and
* is not a feature that should normally be required.
* <p>
* The exception to this process is when a provider throws an {@link AccountStatusException}, in which case no
* further providers in the list will be queried.
*
* Post-authentication, the credentials will be cleared from the returned {@code Authentication} object, if it
* implements the {@link CredentialsContainer} interface. This behaviour can be controlled by modifying the
* {@link #setEraseCredentialsAfterAuthentication(boolean) eraseCredentialsAfterAuthentication} property.
*
* <h2>Event Publishing</h2>
* <p>
* Authentication event publishing is delegated to the configured {@link AuthenticationEventPublisher} which defaults
* to a null implementation which doesn't publish events, so if you are configuring the bean yourself you must inject
* a publisher bean if you want to receive events. The standard implementation is {@link DefaultAuthenticationEventPublisher}
* which maps common exceptions to events (in the case of authentication failure) and publishes an
* {@link org.springframework.security.authentication.event.AuthenticationSuccessEvent AuthenticationSuccessEvent} if
* authentication succeeds. If you are using the namespace then an instance of this bean will be used automatically by
* the <tt><http></tt> configuration, so you will receive events from the web part of your application automatically.
* <p>
* Note that the implementation also publishes authentication failure events when it obtains an authentication result
* (or an exception) from the "parent" {@code AuthenticationManager} if one has been set. So in this situation, the
* parent should not generally be configured to publish events or there will be duplicates.
*
*
* @author Ben Alex
* @author Luke Taylor
*
* @see DefaultAuthenticationEventPublisher
*/
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
//~ Static fields/initializers =====================================================================================
private static final Log logger = LogFactory.getLog(ProviderManager.class);
//~ Instance fields ================================================================================================
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List<AuthenticationProvider> providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
private boolean clearExtraInformation = false;
/**
* @deprecated Use constructor which takes provider list
*/
@Deprecated
public ProviderManager() {
}
public ProviderManager(List<AuthenticationProvider> providers) {
this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
//~ Methods ========================================================================================================
public void afterPropertiesSet() throws Exception {
checkState();
}
private void checkState() {
if (parent == null && providers.isEmpty()) {
throw new IllegalArgumentException("A parent AuthenticationManager or a list " +
"of AuthenticationProviders is required");
}
}
/**
* Attempts to authenticate the passed {@link Authentication} object.
* <p>
* The list of {@link AuthenticationProvider}s will be successively tried until an
* <code>AuthenticationProvider</code> indicates it is capable of authenticating the type of
* <code>Authentication</code> object passed. Authentication will then be attempted with that
* <code>AuthenticationProvider</code>.
* <p>
* If more than one <code>AuthenticationProvider</code> supports the passed <code>Authentication</code>
* object, only the first <code>AuthenticationProvider</code> tried will determine the result. No subsequent
* <code>AuthenticationProvider</code>s will be tried.
*
* @param authentication the authentication request object.
*
* @return a fully authenticated object including credentials.
*
* @throws AuthenticationException if authentication fails.
*/
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to invalid account status
throw e;
} catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
} catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already handled the request
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data from authentication
((CredentialsContainer)result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",
new Object[] {toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
eventPublisher.publishAuthenticationFailure(ex, auth);
ex.setAuthentication(auth);
if (clearExtraInformation) {
ex.clearExtraInformation();
}
}
/**
* Copies the authentication details from a source Authentication object to a destination one, provided the
* latter does not already have one set.
*
* @param source source authentication
* @param dest the destination authentication object
*/
private void copyDetails(Authentication source, Authentication dest) {
if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
token.setDetails(source.getDetails());
}
}
public List<AuthenticationProvider> getProviders() {
return providers;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
/**
* @deprecated Use constructor injection
*/
@Deprecated
public void setParent(AuthenticationManager parent) {
this.parent = parent;
}
public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
/**
* If set to, a resulting {@code Authentication} which implements the {@code CredentialsContainer} interface
* will have its {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called before it is returned
* from the {@code authenticate()} method.
*
* @param eraseSecretData set to {@literal false} to retain the credentials data in memory.
* Defaults to {@literal true}.
*/
public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
this.eraseCredentialsAfterAuthentication = eraseSecretData;
}
public boolean isEraseCredentialsAfterAuthentication() {
return eraseCredentialsAfterAuthentication;
}
/**
* Sets the {@link AuthenticationProvider} objects to be used for authentication.
*
* @param providers the list of authentication providers which will be used to process authentication requests.
*
* @throws IllegalArgumentException if the list is empty or null, or any of the elements in the list is not an
* AuthenticationProvider instance.
* @deprecated Use constructor injection
*/
@Deprecated
@SuppressWarnings({ "unchecked", "rawtypes" })
public void setProviders(List providers) {
Assert.notNull(providers, "Providers list cannot be null");
for(Object currentObject : providers) {
Assert.isInstanceOf(AuthenticationProvider.class, currentObject, "Can only provide AuthenticationProvider instances");
}
this.providers = providers;
}
/**
* If set to true, the {@code extraInformation} set on an {@code AuthenticationException} will be cleared
* before rethrowing it. This is useful for use with remoting protocols where the information shouldn't
* be serialized to the client. Defaults to 'false'.
*
* @see org.springframework.security.core.AuthenticationException#getExtraInformation()
* @deprecated the {@code extraInformation} property is deprecated
*/
@Deprecated
public void setClearExtraInformation(boolean clearExtraInformation) {
this.clearExtraInformation = clearExtraInformation;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {}
public void publishAuthenticationSuccess(Authentication authentication) {}
}
}