diff --git a/changelog.txt b/changelog.txt index 793cf542d3..5408a4618e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,7 @@ Changes in version 0.x (2004-xx-xx) * Added additional DaoAuthenticationProvider event when user not found * Added Authentication.getDetails() to DaoAuthenticationProvider response * Added DaoAuthenticationProvider.hideUserNotFoundExceptions (default=true) +* Added PasswordAuthenticationProvider for password-validating DAOs (eg LDAP) * Extracted removeUserFromCache(String) to UserCache interface * Improved ConfigAttributeEditor so it trims preceding and trailing spaces * Fixed EH-CACHE-based caching implementation behaviour when cache exists diff --git a/contributors.txt b/contributors.txt index b26ca86a12..7e9332ad91 100644 --- a/contributors.txt +++ b/contributors.txt @@ -30,8 +30,9 @@ contributions to the Acegi Security System for Spring project: * Ray Krueger is a current member of the development team. -* Karel Miarka contributed a fix for EH-CACHE NPEs and additional event - handling for DaoAuthenticationProvider. +* Karel Miarka contributed a fix for EH-CACHE NPEs, additional event + handling for DaoAuthenticationProvider, and the + PasswordAuthenticationProvider-related classes * Anyone else I've forgotten (please let me know so I can correct this). diff --git a/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java b/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java new file mode 100644 index 0000000000..fc56936873 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/PasswordAuthenticationDao.java @@ -0,0 +1,76 @@ +/* Copyright 2004 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 net.sf.acegisecurity.providers.dao; + +import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.UserDetails; + +import org.springframework.dao.DataAccessException; + + +/** + * Defines an interface for DAO implementations capable of locating and also + * validating a password. + * + *
+ * Used with the {@link PasswordDaoAuthenticationProvider}. + *
+ * + *+ * The interface requires only one read-only method, which simplifies support + * of new data access strategies. + *
+ * + * @author Karel Miarka + */ +public interface PasswordAuthenticationDao { + //~ Methods ================================================================ + + /** + * Locates the user based on the username and password. In the actual + * implementation, the search may possibly be case sensitive, or case + * insensitive depending on how the implementaion instance is configured. + * In this case, theUserDetails
object that comes back may
+ * have a username that is of a different case than what was actually
+ * requested.
+ *
+ *
+ * The implementation is responsible for password validation. It must throw
+ * BadCredentialsException
(or subclass of that exception if
+ * desired) if the provided password is invalid.
+ *
+ * The implementation is responsible for filling the username and password
+ * parameters into the implementation of UserDetails
.
+ *
GrantedAuthority
s
+ */
+ public UserDetails loadUserByUsernameAndPassword(String username,
+ String password) throws DataAccessException, BadCredentialsException;
+}
diff --git a/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java
new file mode 100644
index 0000000000..a1757b7389
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java
@@ -0,0 +1,276 @@
+/* Copyright 2004 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 net.sf.acegisecurity.providers.dao;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationServiceException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.DisabledException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.providers.AuthenticationProvider;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.cache.NullUserCache;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureUsernameOrPasswordEvent;
+import net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+import org.springframework.dao.DataAccessException;
+
+
+/**
+ * An {@link AuthenticationProvider} implementation that retrieves user details
+ * from a {@link PasswordAuthenticationDao}.
+ *
+ *
+ * This AuthenticationProvider
is capable of validating {@link
+ * UsernamePasswordAuthenticationToken} requests containing the correct
+ * username, password and when the user is not disabled.
+ *
+ * Unlike {@link DaoAuthenticationProvider}, the responsibility for password
+ * validation is delegated to PasswordAuthenticationDao
.
+ *
+ * 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 and password can be validated without needing to query the {@link
+ * PasswordAuthenticationDao}. It should be noted that if a user appears to
+ * present an incorrect password, the {@link PasswordAuthenticationDao} will
+ * be queried to confirm the most up-to-date password was used for comparison.
+ *
+ * If an application context is detected (which is automatically the case when + * the bean is started within a Spring container), application events will be + * published to the context. See {@link + * net.sf.acegisecurity.providers.dao.event.AuthenticationEvent} for further + * information. + *
+ * + * @author Karel Miarka + */ +public class PasswordDaoAuthenticationProvider implements AuthenticationProvider, + InitializingBean, ApplicationContextAware { + //~ Instance fields ======================================================== + + private ApplicationContext context; + private PasswordAuthenticationDao authenticationDao; + private UserCache userCache = new NullUserCache(); + private boolean forcePrincipalAsString = false; + + //~ Methods ================================================================ + + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + + public ApplicationContext getContext() { + return context; + } + + public void setForcePrincipalAsString(boolean forcePrincipalAsString) { + this.forcePrincipalAsString = forcePrincipalAsString; + } + + public boolean isForcePrincipalAsString() { + return forcePrincipalAsString; + } + + public void setPasswordAuthenticationDao( + PasswordAuthenticationDao authenticationDao) { + this.authenticationDao = authenticationDao; + } + + public PasswordAuthenticationDao getPasswordAuthenticationDao() { + return authenticationDao; + } + + public void setUserCache(UserCache userCache) { + this.userCache = userCache; + } + + public UserCache getUserCache() { + return userCache; + } + + public void afterPropertiesSet() throws Exception { + if (this.authenticationDao == null) { + throw new IllegalArgumentException( + "A Password authentication DAO must be set"); + } + + if (this.userCache == null) { + throw new IllegalArgumentException("A user cache must be set"); + } + } + + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + // Determine username + String username = authentication.getPrincipal().toString(); + + if (authentication.getPrincipal() instanceof UserDetails) { + username = ((UserDetails) authentication.getPrincipal()) + .getUsername(); + } + + String password = authentication.getCredentials().toString(); + + boolean cacheWasUsed = true; + UserDetails user = this.userCache.getUserFromCache(username); + + // Check if the provided password is the same as the password in cache + if ((user != null) && !password.equals(user.getPassword())) { + user = null; + this.userCache.removeUserFromCache(username); + } + + if (user == null) { + cacheWasUsed = false; + + try { + user = getUserFromBackend(username, password); + } catch (BadCredentialsException ex) { + if (this.context != null) { + if ((username == null) || "".equals(username)) { + username = "NONE_PROVIDED"; + } + + context.publishEvent(new AuthenticationFailureUsernameOrPasswordEvent( + authentication, + new User(username, "*****", false, + new GrantedAuthority[0]))); + } + + throw ex; + } + } + + if (!user.isEnabled()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureDisabledEvent( + authentication, user)); + } + + throw new DisabledException("User is disabled"); + } + + if (!cacheWasUsed) { + // Put into cache + this.userCache.putUserInCache(user); + + // As this appears to be an initial login, publish the event + if (this.context != null) { + context.publishEvent(new AuthenticationSuccessEvent( + authentication, user)); + } + } + + Object principalToReturn = user; + + if (forcePrincipalAsString) { + principalToReturn = user.getUsername(); + } + + return createSuccessAuthentication(principalToReturn, authentication, + user); + } + + public boolean supports(Class authentication) { + if (UsernamePasswordAuthenticationToken.class.isAssignableFrom( + authentication)) { + return true; + } else { + return false; + } + } + + /** + * Creates a successful {@link Authentication} object. + * + *
+ * Protected so subclasses can override. This might be required if multiple
+ * credentials need to be placed into a custom Authentication
+ * object, such as a password as well as a ZIP code.
+ *
+ * Subclasses will usually store the original credentials the user supplied
+ * (not salted or encoded passwords) in the returned
+ * Authentication
object.
+ *
PasswordDaoAuthenticationProvider
for validation
+ * @param user that was loaded by the
+ * PasswordAuthenticationDao
+ *
+ * @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() != null)
+ ? authentication.getDetails().toString() : null);
+
+ return result;
+ }
+
+ private UserDetails getUserFromBackend(String username, String password) {
+ try {
+ return this.authenticationDao.loadUserByUsernameAndPassword(username,
+ password);
+ } catch (DataAccessException repositoryProblem) {
+ throw new AuthenticationServiceException(repositoryProblem
+ .getMessage(), repositoryProblem);
+ }
+ }
+}
diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java
new file mode 100644
index 0000000000..d0661af000
--- /dev/null
+++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureUsernameOrPasswordEvent.java
@@ -0,0 +1,43 @@
+/* Copyright 2004 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 net.sf.acegisecurity.providers.dao.event;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.UserDetails;
+
+
+/**
+ * Application event which indicates authentication failure due to invalid
+ * username or password.
+ *
+ *
+ * AuthenticationFailureUsernameOrPasswordEvent.getUser()
returns
+ * an instance of User
, where the username is filled by the
+ * String
provided at login attempt. The other properties are set
+ * to non-null
values without any meaning.
+ *