org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider Maven / Gradle / Ivy
/* 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.acegisecurity.providers.dao;
import org.acegisecurity.AccountExpiredException;
import org.acegisecurity.AcegiMessageSource;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.CredentialsExpiredException;
import org.acegisecurity.DisabledException;
import org.acegisecurity.LockedException;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.cache.NullUserCache;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
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.util.Assert;
/**
* A base {@link AuthenticationProvider} that allows subclasses to override and work with {@link
* org.acegisecurity.userdetails.UserDetails} objects. The class is designed to respond to {@link
* UsernamePasswordAuthenticationToken} authentication requests.Upon successful validation, a
* UsernamePasswordAuthenticationToken
will be created and returned to the caller. The token will include
* as its principal either a String
representation of the username, or the {@link UserDetails} that was
* returned from the authentication repository. Using String
is appropriate if a container adapter is
* being used, as it expects String
representations of the username. Using UserDetails
is
* appropriate if you require access to additional properties of the authenticated user, such as email addresses,
* human-friendly names etc. As container adapters are not recommended to be used, and UserDetails
* implementations provide additional flexibility, by default a UserDetails
is returned. To override this
* default, set the {@link #setForcePrincipalAsString} to true
.
* Caching is handled via the UserDetails
object being placed in the {@link UserCache}. This
* ensures that subsequent requests with the same username can be validated without needing to query the {@link
* UserDetailsService}. It should be noted that if a user appears to present an incorrect password, the {@link
* UserDetailsService} will be queried to confirm the most up-to-date password was used for comparison.
*
* @author Ben Alex
* @version $Id: AbstractUserDetailsAuthenticationProvider.java 1674 2006-09-15 03:36:51Z benalex $
*/
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean,
MessageSourceAware {
//~ Instance fields ================================================================================================
protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
//~ Methods ========================================================================================================
/**
* Allows subclasses to perform any additional checks of a returned (or cached) UserDetails
* for a given authentication request. Generally a subclass will at least compare the {@link
* Authentication#getCredentials()} with a {@link UserDetails#getPassword()}. If custom logic is needed to compare
* additional properties of UserDetails
and/or UsernamePasswordAuthenticationToken
,
* these should also appear in this method.
*
* @param userDetails as retrieved from the {@link #retrieveUser(String, UsernamePasswordAuthenticationToken)} or
* UserCache
* @param authentication the current request that needs to be authenticated
*
* @throws AuthenticationException AuthenticationException if the credentials could not be validated (generally a
* BadCredentialsException
, an AuthenticationServiceException
)
*/
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
doAfterPropertiesSet();
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException notFound) {
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
throw notFound;
}
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
if (!user.isAccountNonLocked()) {
throw new LockedException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked",
"User account is locked"));
}
if (!user.isEnabled()) {
throw new DisabledException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled",
"User is disabled"));
}
if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired",
"User account has expired"));
}
// This check must come here, as we don't want to tell users
// about account status unless they presented the correct credentials
try {
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} catch (AuthenticationException exception) {
if(cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (ie not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
} else {
throw exception;
}
}
if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
/**
* Creates a successful {@link Authentication} object.Protected so subclasses can override.
* Subclasses will usually store the original credentials the user supplied (not salted or encoded
* passwords) in the returned Authentication
object.
*
* @param principal that should be the principal in the returned object (defined by the {@link
* #isForcePrincipalAsString()} method)
* @param authentication that was presented to the provider for validation
* @param user that was loaded by the implementation
*
* @return the successful authentication token
*/
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), user.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
protected void doAfterPropertiesSet() throws Exception {}
public UserCache getUserCache() {
return userCache;
}
public boolean isForcePrincipalAsString() {
return forcePrincipalAsString;
}
public boolean isHideUserNotFoundExceptions() {
return hideUserNotFoundExceptions;
}
/**
* Allows subclasses to actually retrieve the UserDetails
from an implementation-specific
* location, with the option of throwing an AuthenticationException
immediately if the presented
* credentials are incorrect (this is especially useful if it is necessary to bind to a resource as the user in
* order to obtain or generate a UserDetails
).Subclasses are not required to perform any
* caching, as the AbstractUserDetailsAuthenticationProvider
will by default cache the
* UserDetails
. The caching of UserDetails
does present additional complexity as this
* means subsequent requests that rely on the cache will need to still have their credentials validated, even if
* the correctness of credentials was assured by subclasses adopting a binding-based strategy in this method.
* Accordingly it is important that subclasses either disable caching (if they want to ensure that this method is
* the only method that is capable of authenticating a request, as no UserDetails
will ever be
* cached) or ensure subclasses implement {@link #additionalAuthenticationChecks(UserDetails,
* UsernamePasswordAuthenticationToken)} to compare the credentials of a cached UserDetails
with
* subsequent authentication requests.
* Most of the time subclasses will not perform credentials inspection in this method, instead
* performing it in {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)} so
* that code related to credentials validation need not be duplicated across two methods.
*
* @param username The username to retrieve
* @param authentication The authentication request, which subclasses may need to perform a binding-based
* retrieval of the UserDetails
*
* @return the user information (never null
- instead an exception should the thrown)
*
* @throws AuthenticationException if the credentials could not be validated (generally a
* BadCredentialsException
, an AuthenticationServiceException
or
* UsernameNotFoundException
)
*/
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
this.forcePrincipalAsString = forcePrincipalAsString;
}
/**
* By default the AbstractUserDetailsAuthenticationProvider
throws a
* BadCredentialsException
if a username is not found or the password is incorrect. Setting this
* property to false
will cause UsernameNotFoundException
s to be thrown instead for the
* former. Note this is considered less secure than throwing BadCredentialsException
for both
* exceptions.
*
* @param hideUserNotFoundExceptions set to false
if you wish UsernameNotFoundException
s
* to be thrown instead of the non-specific BadCredentialsException
(defaults to
* true
)
*/
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
}