org.springframework.security.authentication.ProviderManager Maven / Gradle / Ivy
/*
* Copyright 2002-2020 the original author or authors.
*
* 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
*
* https://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.Arrays;
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.core.log.LogMessage;
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;
import org.springframework.util.CollectionUtils;
/**
* Iterates an {@link Authentication} request through a list of
* {@link AuthenticationProvider}s.
*
*
* AuthenticationProviders 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 AuthenticationException
,
* the last AuthenticationException
received will be used. If no provider
* returns a non-null response, or indicates it can even process an
* Authentication
, the ProviderManager
will throw a
* ProviderNotFoundException
. 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.
*
* 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.
*
*
Event Publishing
*
* 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 <http>
* configuration, so you will receive events from the web part of your application
* automatically.
*
* 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 {
private static final Log logger = LogFactory.getLog(ProviderManager.class);
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private List providers = Collections.emptyList();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private AuthenticationManager parent;
private boolean eraseCredentialsAfterAuthentication = true;
/**
* Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
* @param providers the {@link AuthenticationProvider}s to use
*/
public ProviderManager(AuthenticationProvider... providers) {
this(Arrays.asList(providers), null);
}
/**
* Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
* @param providers the {@link AuthenticationProvider}s to use
*/
public ProviderManager(List providers) {
this(providers, null);
}
/**
* Construct a {@link ProviderManager} using the provided parameters
* @param providers the {@link AuthenticationProvider}s to use
* @param parent a parent {@link AuthenticationManager} to fall back to
*/
public ProviderManager(List providers, AuthenticationManager parent) {
Assert.notNull(providers, "providers list cannot be null");
this.providers = providers;
this.parent = parent;
checkState();
}
@Override
public void afterPropertiesSet() {
checkState();
}
private void checkState() {
Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
"A parent AuthenticationManager or a list of AuthenticationProviders is required");
Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
"providers list cannot contain null values");
}
/**
* Attempts to authenticate the passed {@link Authentication} object.
*
* The list of {@link AuthenticationProvider}s will be successively tried until an
* AuthenticationProvider
indicates it is capable of authenticating the
* type of Authentication
object passed. Authentication will then be
* attempted with that AuthenticationProvider
.
*
* If more than one AuthenticationProvider
supports the passed
* Authentication
object, the first one able to successfully authenticate
* the Authentication
object determines the result
,
* overriding any possible AuthenticationException
thrown by earlier
* supporting AuthenticationProvider
s. On successful authentication, no
* subsequent AuthenticationProvider
s will be tried. If authentication
* was not successful by any supporting AuthenticationProvider
the last
* thrown AuthenticationException
will be rethrown.
* @param authentication the authentication request object.
* @return a fully authenticated object including credentials.
* @throws AuthenticationException if authentication fails.
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
}
catch (AuthenticationException ex) {
lastException = ex;
}
}
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
}
catch (ProviderNotFoundException ex) {
// 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 ex) {
parentException = ex;
lastException = ex;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
@SuppressWarnings("deprecation")
private void prepareException(AuthenticationException ex, Authentication auth) {
this.eventPublisher.publishAuthenticationFailure(ex, auth);
}
/**
* 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 getProviders() {
return this.providers;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
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 this.eraseCredentialsAfterAuthentication;
}
private static final class NullEventPublisher implements AuthenticationEventPublisher {
@Override
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
}
}
}