org.springframework.security.authentication.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
*
* 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.dao;
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.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.util.Assert;
/**
* A base {@link AuthenticationProvider} that allows subclasses to override and work with
* {@link org.springframework.security.core.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 by storing 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.
* Caching is only likely to be required for stateless applications. In a normal web
* application, for example, the SecurityContext is stored in the user's session
* and the user isn't reauthenticated on each request. The default cache implementation is
* therefore {@link NullUserCache}.
*
* @author Ben Alex
*/
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
/**
* 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;
@Override
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();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
private String determineUsername(Authentication authentication) {
return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
}
/**
* 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(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
protected void doAfterPropertiesSet() throws Exception {
}
public UserCache getUserCache() {
return this.userCache;
}
public boolean isForcePrincipalAsString() {
return this.forcePrincipalAsString;
}
public boolean isHideUserNotFoundExceptions() {
return this.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;
}
@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
@Override
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
protected UserDetailsChecker getPreAuthenticationChecks() {
return this.preAuthenticationChecks;
}
/**
* Sets the policy will be used to verify the status of the loaded
* UserDetails before validation of the credentials takes place.
* @param preAuthenticationChecks strategy to be invoked prior to authentication.
*/
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
this.preAuthenticationChecks = preAuthenticationChecks;
}
protected UserDetailsChecker getPostAuthenticationChecks() {
return this.postAuthenticationChecks;
}
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
this.postAuthenticationChecks = postAuthenticationChecks;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
}
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
}
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
@Override
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
}
}