blob: 06202dc7aebb4b1f8d8dd3747bf13bcc7dce5f9b [file] [log] [blame]
package org.taverna.server.master.identity;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PreDestroy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
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.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.taverna.server.master.utils.CallTimeLogger.PerfLogged;
/**
* A stripped down version of a
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
* DaoAuthenticationProvider}/
* {@link org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
* AbstractUserDetailsAuthenticationProvider} that avoids much of the overhead
* associated with that class.
*/
public class StrippedDownAuthProvider implements AuthenticationProvider {
/**
* The plaintext password used to perform
* {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
* the user is not found to avoid SEC-2056.
*/
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
/**
* The password used to perform
* {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when
* the user is not found to avoid SEC-2056. This is necessary, because some
* {@link PasswordEncoder} implementations will short circuit if the
* password is not in a valid format.
*/
private String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private PasswordEncoder passwordEncoder;
private Map<String, AuthCacheEntry> authCache = new HashMap<>();
protected final Log logger = LogFactory.getLog(getClass());
private static class AuthCacheEntry {
private String creds;
private long timestamp;
private static final long VALIDITY = 1000 * 60 * 20;
AuthCacheEntry(String credentials) {
creds = credentials;
timestamp = System.currentTimeMillis();
}
boolean valid(String password) {
return creds.equals(password) && timestamp+VALIDITY > System.currentTimeMillis();
}
}
@PerfLogged
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (!(authentication instanceof UsernamePasswordAuthenticationToken))
throw new IllegalArgumentException(
"can only authenticate against username+password");
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
// Determine username
String username = (auth.getPrincipal() == null) ? "NONE_PROVIDED"
: auth.getName();
UserDetails user;
try {
user = retrieveUser(username, auth);
if (user == null)
throw new IllegalStateException(
"retrieveUser returned null - a violation of the interface contract");
} catch (UsernameNotFoundException notFound) {
if (logger.isDebugEnabled())
logger.debug("User '" + username + "' not found", notFound);
throw new BadCredentialsException("Bad credentials");
}
// Pre-auth
if (!user.isAccountNonLocked())
throw new LockedException("User account is locked");
if (!user.isEnabled())
throw new DisabledException("User account is disabled");
if (!user.isAccountNonExpired())
throw new AccountExpiredException("User account has expired");
Object credentials = auth.getCredentials();
if (credentials == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException("Bad credentials");
}
String providedPassword = credentials.toString();
boolean matched = false;
synchronized (authCache) {
AuthCacheEntry pw = authCache.get(username);
if (pw != null && providedPassword != null) {
if (pw.valid(providedPassword))
matched = true;
else
authCache.remove(username);
}
}
// Auth
if (!matched) {
if (!passwordEncoder.matches(providedPassword, user.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException("Bad credentials");
}
if (providedPassword != null)
synchronized (authCache) {
authCache.put(username, new AuthCacheEntry(providedPassword));
}
}
// Post-auth
if (!user.isCredentialsNonExpired())
throw new CredentialsExpiredException(
"User credentials have expired");
return createSuccessAuthentication(user, auth, user);
}
@PreDestroy
void clearCache() {
authCache.clear();
}
/**
* Creates a successful {@link Authentication} object.
* <p>
* Protected so subclasses can override.
* </p>
* <p>
* Subclasses will usually store the original credentials the user supplied
* (not salted or encoded passwords) in the returned
* <code>Authentication</code> object.
* </p>
*
* @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
*/
private 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;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication);
}
/**
* Allows subclasses to actually retrieve the <code>UserDetails</code> from
* an implementation-specific location, with the option of throwing an
* <code>AuthenticationException</code> 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
* <code>UserDetails</code>).
* <p>
* Subclasses are not required to perform any caching, as the
* <code>AbstractUserDetailsAuthenticationProvider</code> will by default
* cache the <code>UserDetails</code>. The caching of
* <code>UserDetails</code> 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 <code>UserDetails</code> will ever be
* cached) or ensure subclasses implement
* {@link #additionalAuthenticationChecks(UserDetails, UsernamePasswordAuthenticationToken)}
* to compare the credentials of a cached <code>UserDetails</code> with
* subsequent authentication requests.
* </p>
* <p>
* 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.
* </p>
*
* @param username
* The username to retrieve
* @param authentication
* The authentication request, which subclasses <em>may</em> need
* to perform a binding-based retrieval of the
* <code>UserDetails</code>
*
* @return the user information (never <code>null</code> - instead an
* exception should the thrown)
*
* @throws AuthenticationException
* if the credentials could not be validated (generally a
* <code>BadCredentialsException</code>, an
* <code>AuthenticationServiceException</code> or
* <code>UsernameNotFoundException</code>)
*/
private UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
try {
return userDetailsService.loadUserByUsername(username);
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials()
.toString();
passwordEncoder.matches(presentedPassword,
userNotFoundEncodedPassword);
}
throw notFound;
} catch (AuthenticationException e) {
throw e;
} catch (Exception repositoryProblem) {
throw new AuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
}
/**
* Sets the PasswordEncoder instance to be used to encode and validate
* passwords.
*/
@Required
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
if (passwordEncoder == null)
throw new IllegalArgumentException("passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = passwordEncoder
.encode(USER_NOT_FOUND_PASSWORD);
}
@Required
public void setUserDetailsService(UserDetailsService userDetailsService) {
if (userDetailsService == null)
throw new IllegalStateException("A UserDetailsService must be set");
this.userDetailsService = userDetailsService;
}
}