diff --git a/core/src/main/java/org/acegisecurity/AccountExpiredException.java b/core/src/main/java/org/acegisecurity/AccountExpiredException.java new file mode 100644 index 0000000000..b2dce386f9 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/AccountExpiredException.java @@ -0,0 +1,49 @@ +/* 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; + +/** + * Thrown if an authentication request is rejected because the account has + * expired. Makes no assertion as to whether or not the credentials were + * valid. + * + * @author Ben Alex + * @version $Id$ + */ +public class AccountExpiredException extends AuthenticationException { + //~ Constructors =========================================================== + + /** + * Constructs a AccountExpiredException with the specified + * message. + * + * @param msg the detail message + */ + public AccountExpiredException(String msg) { + super(msg); + } + + /** + * Constructs a AccountExpiredException with the specified + * message and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public AccountExpiredException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/core/src/main/java/org/acegisecurity/CredentialsExpiredException.java b/core/src/main/java/org/acegisecurity/CredentialsExpiredException.java new file mode 100644 index 0000000000..5cdba82878 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/CredentialsExpiredException.java @@ -0,0 +1,49 @@ +/* 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; + +/** + * Thrown if an authentication request is rejected because the account's + * credentials have expired. Makes no assertion as to whether or not the + * credentials were valid. + * + * @author Ben Alex + * @version $Id$ + */ +public class CredentialsExpiredException extends AuthenticationException { + //~ Constructors =========================================================== + + /** + * Constructs a CredentialsExpiredException with the specified + * message. + * + * @param msg the detail message + */ + public CredentialsExpiredException(String msg) { + super(msg); + } + + /** + * Constructs a CredentialsExpiredException with the specified + * message and root cause. + * + * @param msg the detail message + * @param t root cause + */ + public CredentialsExpiredException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java index 581e3dd20d..c4b5802a2f 100644 --- a/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java @@ -15,16 +15,20 @@ package net.sf.acegisecurity.providers.dao; +import net.sf.acegisecurity.AccountExpiredException; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationException; import net.sf.acegisecurity.AuthenticationServiceException; import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.CredentialsExpiredException; 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.AuthenticationFailureAccountExpiredEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureCredentialsExpiredEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationFailurePasswordEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureUsernameNotFoundEvent; @@ -228,7 +232,7 @@ public class DaoAuthenticationProvider implements AuthenticationProvider, authentication, new User("".equals(username) ? "EMPTY_STRING_PROVIDED" : username, "*****", - false, new GrantedAuthority[0]))); + false, false, false, new GrantedAuthority[0]))); } throw ex; @@ -244,6 +248,25 @@ public class DaoAuthenticationProvider implements AuthenticationProvider, throw new DisabledException("User is disabled"); } + if (!user.isAccountNonExpired()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureAccountExpiredEvent( + authentication, user)); + } + + throw new AccountExpiredException("User account has expired"); + } + + if (!user.isCredentialsNonExpired()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureCredentialsExpiredEvent( + authentication, user)); + } + + throw new CredentialsExpiredException( + "User credentials have expired"); + } + if (!isPasswordCorrect(authentication, user)) { // Password incorrect, so ensure we're using most current password if (cacheWasUsed) { diff --git a/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java index a1757b7389..943e96a07d 100644 --- a/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java +++ b/core/src/main/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProvider.java @@ -15,16 +15,20 @@ package net.sf.acegisecurity.providers.dao; +import net.sf.acegisecurity.AccountExpiredException; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationException; import net.sf.acegisecurity.AuthenticationServiceException; import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.CredentialsExpiredException; 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.AuthenticationFailureAccountExpiredEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureCredentialsExpiredEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureUsernameOrPasswordEvent; import net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent; @@ -179,7 +183,7 @@ public class PasswordDaoAuthenticationProvider implements AuthenticationProvider context.publishEvent(new AuthenticationFailureUsernameOrPasswordEvent( authentication, - new User(username, "*****", false, + new User(username, "*****", false, false, false, new GrantedAuthority[0]))); } @@ -196,6 +200,25 @@ public class PasswordDaoAuthenticationProvider implements AuthenticationProvider throw new DisabledException("User is disabled"); } + if (!user.isAccountNonExpired()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureAccountExpiredEvent( + authentication, user)); + } + + throw new AccountExpiredException("User account has expired"); + } + + if (!user.isCredentialsNonExpired()) { + if (this.context != null) { + context.publishEvent(new AuthenticationFailureCredentialsExpiredEvent( + authentication, user)); + } + + throw new CredentialsExpiredException( + "User credentials have expired"); + } + if (!cacheWasUsed) { // Put into cache this.userCache.putUserInCache(user); diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureAccountExpiredEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureAccountExpiredEvent.java new file mode 100644 index 0000000000..8bc4bc8965 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureAccountExpiredEvent.java @@ -0,0 +1,37 @@ +/* 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 the user's + * account having expired. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationFailureAccountExpiredEvent + extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationFailureAccountExpiredEvent( + Authentication authentication, UserDetails user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureCredentialsExpiredEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureCredentialsExpiredEvent.java new file mode 100644 index 0000000000..ff2fb44c29 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureCredentialsExpiredEvent.java @@ -0,0 +1,37 @@ +/* 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 the user's + * credentials having expired. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationFailureCredentialsExpiredEvent + extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationFailureCredentialsExpiredEvent( + Authentication authentication, UserDetails user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/userdetails/User.java b/core/src/main/java/org/acegisecurity/userdetails/User.java index 7e353b8204..bc1eb2e384 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/User.java +++ b/core/src/main/java/org/acegisecurity/userdetails/User.java @@ -37,6 +37,8 @@ public class User implements UserDetails { private String password; private String username; private GrantedAuthority[] authorities; + private boolean accountNonExpired; + private boolean credentialsNonExpired; private boolean enabled; //~ Constructors =========================================================== @@ -57,8 +59,38 @@ public class User implements UserDetails { * @throws IllegalArgumentException if a null value was passed * either as a parameter or as an element in the * GrantedAuthority[] array + * + * @deprecated use new constructor with extended properties (this + * constructor will be removed from release 1.0.0) */ public User(String username, String password, boolean enabled, + GrantedAuthority[] authorities) throws IllegalArgumentException { + this(username, password, enabled, true, true, authorities); + } + + /** + * Construct the User with the details required by {@link + * DaoAuthenticationProvider}. + * + * @param username the username presented to the + * DaoAuthenticationProvider + * @param password the password that should be presented to the + * DaoAuthenticationProvider + * @param enabled set to true if the user is enabled + * @param accountNonExpired set to true if the account has not + * expired + * @param credentialsNonExpired set to true if the credentials + * have not expired + * @param authorities the authorities that should be granted to the caller + * if they presented the correct username and password and the user + * is enabled + * + * @throws IllegalArgumentException if a null value was passed + * either as a parameter or as an element in the + * GrantedAuthority[] array + */ + public User(String username, String password, boolean enabled, + boolean accountNonExpired, boolean credentialsNonExpired, GrantedAuthority[] authorities) throws IllegalArgumentException { if (((username == null) || "".equals(username)) || (password == null) || "".equals(password) || (authorities == null)) { @@ -78,6 +110,8 @@ public class User implements UserDetails { this.password = password; this.enabled = enabled; this.authorities = authorities; + this.accountNonExpired = accountNonExpired; + this.credentialsNonExpired = credentialsNonExpired; } protected User() { @@ -86,10 +120,18 @@ public class User implements UserDetails { //~ Methods ================================================================ + public boolean isAccountNonExpired() { + return accountNonExpired; + } + public GrantedAuthority[] getAuthorities() { return authorities; } + public boolean isCredentialsNonExpired() { + return credentialsNonExpired; + } + public boolean isEnabled() { return enabled; } diff --git a/core/src/main/java/org/acegisecurity/userdetails/UserDetails.java b/core/src/main/java/org/acegisecurity/userdetails/UserDetails.java index c3d1daa7e5..93bfe58929 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/UserDetails.java +++ b/core/src/main/java/org/acegisecurity/userdetails/UserDetails.java @@ -43,6 +43,16 @@ import java.io.Serializable; public interface UserDetails extends Serializable { //~ Methods ================================================================ + /** + * Indicates whether the user's account has expired. An expired account + * cannot be authenticated. + * + * @return true if the user's account is valid (ie + * non-expired), false if no longer valid (ie + * expired) + */ + public boolean isAccountNonExpired(); + /** * Returns the authorities granted to the user. Cannot return * null. @@ -51,6 +61,16 @@ public interface UserDetails extends Serializable { */ public GrantedAuthority[] getAuthorities(); + /** + * Indicates whether the user's credentials (password) has expired. Expired + * credentials prevent authentication. + * + * @return true if the user's credentials are valid (ie + * non-expired), false if no longer valid (ie + * expired) + */ + public boolean isCredentialsNonExpired(); + /** * Indicates whether the user is enabled or disabled. A disabled user * cannot be authenticated. diff --git a/core/src/main/java/org/acegisecurity/userdetails/jdbc/JdbcDaoImpl.java b/core/src/main/java/org/acegisecurity/userdetails/jdbc/JdbcDaoImpl.java index f435b63228..0dc668bbad 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/jdbc/JdbcDaoImpl.java +++ b/core/src/main/java/org/acegisecurity/userdetails/jdbc/JdbcDaoImpl.java @@ -58,6 +58,13 @@ import javax.sql.DataSource; * the {@link MappingSqlQuery} instances used, via the {@link * #initMappingSqlQueries()} extension point. *

+ * + *

+ * In order to minimise backward compatibility issues, this DAO does not + * recognise the expiration of user accounts or the expiration of user + * credentials. However, it does recognise and honour the user + * enabled/disabled column. + *

* * @author Ben Alex * @author colin sampaleanu @@ -185,14 +192,14 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements AuthenticationDao { arrayAuths = (GrantedAuthority[]) dbAuths.toArray(arrayAuths); return new User(user.getUsername(), user.getPassword(), - user.isEnabled(), arrayAuths); + user.isEnabled(), true, true, arrayAuths); } /** * Allows subclasses to add their own granted authorities to the list to be * returned in the User. * - * @param username the username, for use by finder methods + * @param username the username, for use by finder methods * @param authorities the current granted authorities, as populated from * the authoritiesByUsername mapping */ @@ -248,7 +255,8 @@ public class JdbcDaoImpl extends JdbcDaoSupport implements AuthenticationDao { String username = rs.getString(1); String password = rs.getString(2); boolean enabled = rs.getBoolean(3); - UserDetails user = new User(username, password, enabled, + UserDetails user = new User(username, password, enabled, true, + true, new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")}); return user; diff --git a/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java b/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java index a9eb6341d1..cc402f9924 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java +++ b/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java @@ -56,6 +56,12 @@ import java.util.Properties; * If the above requirements are not met, the invalid entry will be silently * ignored. *

+ * + *

+ * This editor always assumes each entry has a non-expired account and + * non-expired credentials. However, it does honour the user enabled/disabled + * flag as described above. + *

* * @author Ben Alex * @version $Id$ @@ -91,7 +97,7 @@ public class UserMapEditor extends PropertyEditorSupport { // Make a user object, assuming the properties were properly provided if (attr != null) { UserDetails user = new User(username, attr.getPassword(), - attr.isEnabled(), attr.getAuthorities()); + attr.isEnabled(), true, true, attr.getAuthorities()); userMap.addUser(user); } } diff --git a/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java b/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java index deada7925e..5e856f0eb9 100644 --- a/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java +++ b/core/src/test/java/org/acegisecurity/acl/basic/GrantedAuthorityEffectiveAclsResolverTests.java @@ -55,7 +55,7 @@ public class GrantedAuthorityEffectiveAclsResolverTests extends TestCase { .getPrincipal(), new NamedEntityObjectIdentity("OBJECT", "100"), null, 2); private UsernamePasswordAuthenticationToken scottWithUserDetails = new UsernamePasswordAuthenticationToken(new User( - "scott", "NOT_USED", true, + "scott", "NOT_USED", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl( "ROLE_EVERYBODY")}), "not used", new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_EVERYBODY"), new GrantedAuthorityImpl("ROLE_TWO")}); diff --git a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java index 091af44471..3a3dcaa6e1 100644 --- a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java +++ b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java @@ -327,7 +327,7 @@ public class CasAuthenticationProviderTests extends TestCase { } private UserDetails makeUserDetails() { - return new User("user", "password", true, + return new User("user", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } @@ -337,7 +337,7 @@ public class CasAuthenticationProviderTests extends TestCase { private class MockAuthoritiesPopulator implements CasAuthoritiesPopulator { public UserDetails getUserDetails(String casUserId) throws AuthenticationException { - return new User("user", "password", true, + return new User("user", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl( "ROLE_B")}); } diff --git a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java index 38ac1f06df..f1af3dc848 100644 --- a/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java +++ b/core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java @@ -319,7 +319,7 @@ public class CasAuthenticationTokenTests extends TestCase { } private UserDetails makeUserDetails() { - return new User("user", "password", true, + return new User("user", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } diff --git a/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java b/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java index 660ba7931a..57a797252e 100644 --- a/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java +++ b/core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java @@ -102,7 +102,7 @@ public class EhCacheBasedTicketCacheTests extends TestCase { List proxyList = new Vector(); proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check"); - User user = new User("marissa", "password", true, + User user = new User("marissa", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); diff --git a/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java b/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java index e697984459..304e2397d1 100644 --- a/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java +++ b/core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java @@ -139,7 +139,7 @@ public class DaoCasAuthoritiesPopulatorTests extends TestCase { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { if ("marissa".equals(username)) { - return new User("marissa", "koala", true, + return new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { diff --git a/core/src/test/java/org/acegisecurity/providers/dao/DaoAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/dao/DaoAuthenticationProviderTests.java index 5a624a89a1..3f818b2960 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/DaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/DaoAuthenticationProviderTests.java @@ -17,9 +17,11 @@ package net.sf.acegisecurity.providers.dao; import junit.framework.TestCase; +import net.sf.acegisecurity.AccountExpiredException; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationServiceException; import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.CredentialsExpiredException; import net.sf.acegisecurity.DisabledException; import net.sf.acegisecurity.GrantedAuthority; import net.sf.acegisecurity.GrantedAuthorityImpl; @@ -73,6 +75,38 @@ public class DaoAuthenticationProviderTests extends TestCase { } } + public void testAuthenticateFailsIfAccountExpired() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", + "opal"); + + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setAuthenticationDao(new MockAuthenticationDaoUserPeterAccountExpired()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown AccountExpiredException"); + } catch (AccountExpiredException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsIfCredentialsExpired() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", + "opal"); + + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setAuthenticationDao(new MockAuthenticationDaoUserPeterCredentialsExpired()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown CredentialsExpiredException"); + } catch (CredentialsExpiredException expected) { + assertTrue(true); + } + } + public void testAuthenticateFailsIfUserDisabled() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); @@ -426,7 +460,7 @@ public class DaoAuthenticationProviderTests extends TestCase { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { if ("marissa".equals(username)) { - return new User("marissa", password, true, + return new User("marissa", password, true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { @@ -442,6 +476,7 @@ public class DaoAuthenticationProviderTests extends TestCase { throws UsernameNotFoundException, DataAccessException { if ("marissa".equals(username)) { return new User("marissa", "koala{SYSTEM_SALT_VALUE}", true, + true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { @@ -455,7 +490,37 @@ public class DaoAuthenticationProviderTests extends TestCase { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { if ("peter".equals(username)) { - return new User("peter", "opal", false, + return new User("peter", "opal", false, true, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } + + private class MockAuthenticationDaoUserPeterAccountExpired + implements AuthenticationDao { + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + if ("peter".equals(username)) { + return new User("peter", "opal", true, false, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } + + private class MockAuthenticationDaoUserPeterCredentialsExpired + implements AuthenticationDao { + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + if ("peter".equals(username)) { + return new User("peter", "opal", true, true, false, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { diff --git a/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java index 12521b00c3..18a71cca8b 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/PasswordDaoAuthenticationProviderTests.java @@ -17,9 +17,11 @@ package net.sf.acegisecurity.providers.dao; import junit.framework.TestCase; +import net.sf.acegisecurity.AccountExpiredException; import net.sf.acegisecurity.Authentication; import net.sf.acegisecurity.AuthenticationServiceException; import net.sf.acegisecurity.BadCredentialsException; +import net.sf.acegisecurity.CredentialsExpiredException; import net.sf.acegisecurity.DisabledException; import net.sf.acegisecurity.GrantedAuthority; import net.sf.acegisecurity.GrantedAuthorityImpl; @@ -68,6 +70,38 @@ public class PasswordDaoAuthenticationProviderTests extends TestCase { } } + public void testAuthenticateFailsIfAccountExpired() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", + "opal"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserPeterAccountExpired()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown AccountExpiredException"); + } catch (AccountExpiredException expected) { + assertTrue(true); + } + } + + public void testAuthenticateFailsIfCredentialsExpired() { + UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", + "opal"); + + PasswordDaoAuthenticationProvider provider = new PasswordDaoAuthenticationProvider(); + provider.setPasswordAuthenticationDao(new MockAuthenticationDaoUserPeterCredentialsExpired()); + provider.setUserCache(new MockUserCache()); + + try { + provider.authenticate(token); + fail("Should have thrown CredentialsExpiredException"); + } catch (CredentialsExpiredException expected) { + assertTrue(true); + } + } + public void testAuthenticateFailsIfUserDisabled() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("peter", "opal"); @@ -290,7 +324,7 @@ public class PasswordDaoAuthenticationProviderTests extends TestCase { String password) throws BadCredentialsException, DataAccessException { if ("marissa".equals(username) && "koala".equals(password)) { - return new User("marissa", "koala", true, + return new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { @@ -305,7 +339,7 @@ public class PasswordDaoAuthenticationProviderTests extends TestCase { String password) throws BadCredentialsException, DataAccessException { if ("peter".equals(username) && "opal".equals(password)) { - return new User("peter", "opal", false, + return new User("peter", "opal", false, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } else { @@ -314,6 +348,38 @@ public class PasswordDaoAuthenticationProviderTests extends TestCase { } } + private class MockAuthenticationDaoUserPeterAccountExpired + implements PasswordAuthenticationDao { + public UserDetails loadUserByUsernameAndPassword(String username, + String password) + throws UsernameNotFoundException, DataAccessException { + if ("peter".equals(username)) { + return new User("peter", "opal", true, false, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } + + private class MockAuthenticationDaoUserPeterCredentialsExpired + implements PasswordAuthenticationDao { + public UserDetails loadUserByUsernameAndPassword(String username, + String password) + throws UsernameNotFoundException, DataAccessException { + if ("peter".equals(username)) { + return new User("peter", "opal", true, true, false, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } + private class MockUserCache implements UserCache { private Map cache = new HashMap(); diff --git a/core/src/test/java/org/acegisecurity/providers/dao/UserTests.java b/core/src/test/java/org/acegisecurity/providers/dao/UserTests.java index 881d6829d5..0a4744508e 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/UserTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/UserTests.java @@ -60,7 +60,7 @@ public class UserTests extends TestCase { public void testNullValuesRejected() throws Exception { try { - UserDetails user = new User(null, "koala", true, + UserDetails user = new User(null, "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); fail("Should have thrown IllegalArgumentException"); @@ -69,7 +69,7 @@ public class UserTests extends TestCase { } try { - UserDetails user = new User("marissa", null, true, + UserDetails user = new User("marissa", null, true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); fail("Should have thrown IllegalArgumentException"); @@ -78,14 +78,15 @@ public class UserTests extends TestCase { } try { - UserDetails user = new User("marissa", "koala", true, null); + UserDetails user = new User("marissa", "koala", true, true, true, + null); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { assertTrue(true); } try { - UserDetails user = new User("marissa", "koala", true, + UserDetails user = new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), null}); fail("Should have thrown IllegalArgumentException"); } catch (IllegalArgumentException expected) { @@ -96,7 +97,7 @@ public class UserTests extends TestCase { public void testNullWithinGrantedAuthorityElementIsRejected() throws Exception { try { - UserDetails user = new User(null, "koala", true, + UserDetails user = new User(null, "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO"), null, new GrantedAuthorityImpl( "ROLE_THREE")}); @@ -107,7 +108,7 @@ public class UserTests extends TestCase { } public void testUserGettersSetter() throws Exception { - UserDetails user = new User("marissa", "koala", true, + UserDetails user = new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); assertEquals("marissa", user.getUsername()); @@ -120,7 +121,7 @@ public class UserTests extends TestCase { } public void testUserIsEnabled() throws Exception { - UserDetails user = new User("marissa", "koala", false, + UserDetails user = new User("marissa", "koala", false, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); assertTrue(!user.isEnabled()); diff --git a/core/src/test/java/org/acegisecurity/providers/dao/cache/EhCacheBasedUserCacheTests.java b/core/src/test/java/org/acegisecurity/providers/dao/cache/EhCacheBasedUserCacheTests.java index 80ffe11137..3367a9e1b0 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/cache/EhCacheBasedUserCacheTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/cache/EhCacheBasedUserCacheTests.java @@ -95,7 +95,7 @@ public class EhCacheBasedUserCacheTests extends TestCase { } private User getUser() { - return new User("john", "password", true, + return new User("john", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } diff --git a/core/src/test/java/org/acegisecurity/providers/dao/cache/NullUserCacheTests.java b/core/src/test/java/org/acegisecurity/providers/dao/cache/NullUserCacheTests.java index 9c5df76a05..920b31a35f 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/cache/NullUserCacheTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/cache/NullUserCacheTests.java @@ -57,7 +57,7 @@ public class NullUserCacheTests extends TestCase { } private User getUser() { - return new User("john", "password", true, + return new User("john", "password", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); } diff --git a/core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java b/core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java index e8963c76df..6cdc804226 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java @@ -97,7 +97,7 @@ public class AuthenticationEventTests extends TestCase { } private User getUser() { - User user = new User("foo", "bar", true, + User user = new User("foo", "bar", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOOBAR")}); return user; diff --git a/core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java b/core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java index 434e303637..323c180f29 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java @@ -90,7 +90,7 @@ public class LoggerListenerTests extends TestCase { } private User getUser() { - User user = new User("foo", "bar", true, + User user = new User("foo", "bar", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOOBAR")}); return user; diff --git a/core/src/test/java/org/acegisecurity/providers/dao/memory/UserMapTests.java b/core/src/test/java/org/acegisecurity/providers/dao/memory/UserMapTests.java index 54039e15e0..06150601d5 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/memory/UserMapTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/memory/UserMapTests.java @@ -52,13 +52,13 @@ public class UserMapTests extends TestCase { } public void testAddAndRetrieveUser() { - UserDetails marissa = new User("marissa", "koala", true, + UserDetails marissa = new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); - UserDetails scott = new User("scott", "wombat", true, + UserDetails scott = new User("scott", "wombat", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_THREE")}); - UserDetails peter = new User("peter", "opal", true, + UserDetails peter = new User("peter", "opal", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_FOUR")}); UserMap map = new UserMap(); @@ -85,7 +85,7 @@ public class UserMapTests extends TestCase { } public void testUnknownUserIsNotRetrieved() { - UserDetails marissa = new User("marissa", "koala", true, + UserDetails marissa = new User("marissa", "koala", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( "ROLE_TWO")}); UserMap map = new UserMap(); diff --git a/core/src/test/java/org/acegisecurity/providers/dao/salt/ReflectionSaltSourceTests.java b/core/src/test/java/org/acegisecurity/providers/dao/salt/ReflectionSaltSourceTests.java index b646c93ac4..febd64af75 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/salt/ReflectionSaltSourceTests.java +++ b/core/src/test/java/org/acegisecurity/providers/dao/salt/ReflectionSaltSourceTests.java @@ -67,7 +67,7 @@ public class ReflectionSaltSourceTests extends TestCase { ReflectionSaltSource saltSource = new ReflectionSaltSource(); saltSource.setUserPropertyToUse("getDoesNotExist"); - UserDetails user = new User("scott", "wombat", true, + UserDetails user = new User("scott", "wombat", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")}); try { @@ -89,7 +89,7 @@ public class ReflectionSaltSourceTests extends TestCase { saltSource.setUserPropertyToUse("getUsername"); saltSource.afterPropertiesSet(); - UserDetails user = new User("scott", "wombat", true, + UserDetails user = new User("scott", "wombat", true, true, true, new GrantedAuthority[] {new GrantedAuthorityImpl("HOLDER")}); assertEquals("scott", saltSource.getSalt(user)); } diff --git a/core/src/test/java/org/acegisecurity/taglibs/authz/AuthenticationTagTests.java b/core/src/test/java/org/acegisecurity/taglibs/authz/AuthenticationTagTests.java index 52f0efa428..717c667515 100644 --- a/core/src/test/java/org/acegisecurity/taglibs/authz/AuthenticationTagTests.java +++ b/core/src/test/java/org/acegisecurity/taglibs/authz/AuthenticationTagTests.java @@ -77,7 +77,7 @@ public class AuthenticationTagTests extends TestCase { public void testOperationWhenPrincipalIsAUserDetailsInstance() throws JspException { Authentication auth = new TestingAuthenticationToken(new User( - "marissaUserDetails", "koala", true, + "marissaUserDetails", "koala", true, true, true, new GrantedAuthority[] {}), "koala", new GrantedAuthority[] {}); SecureContext sc = new SecureContextImpl(); diff --git a/core/src/test/java/org/acegisecurity/ui/wrapper/ContextHolderAwareRequestWrapperTests.java b/core/src/test/java/org/acegisecurity/ui/wrapper/ContextHolderAwareRequestWrapperTests.java index 13aa9a8505..59b2011be8 100644 --- a/core/src/test/java/org/acegisecurity/ui/wrapper/ContextHolderAwareRequestWrapperTests.java +++ b/core/src/test/java/org/acegisecurity/ui/wrapper/ContextHolderAwareRequestWrapperTests.java @@ -78,7 +78,7 @@ public class ContextHolderAwareRequestWrapperTests extends TestCase { throws Exception { SecureContext sc = new SecureContextImpl(); Authentication auth = new TestingAuthenticationToken(new User( - "marissaAsUserDetails", "koala", true, + "marissaAsUserDetails", "koala", true, true, true, new GrantedAuthority[] {}), "koala", new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_HELLO"), new GrantedAuthorityImpl( "ROLE_FOOBAR")}); diff --git a/doc/docbook/index.xml b/doc/docbook/index.xml index f71c342fe5..01e516ac3a 100644 --- a/doc/docbook/index.xml +++ b/doc/docbook/index.xml @@ -1042,7 +1042,11 @@ public aspect DomainObjectInstanceSecurityAspect implements InitializingBean { authentication is denied. An AuthenticationServiceException is also provided, which indicates the authentication system could not process the - request (eg a database was unavailable). + request (eg a database was unavailable). + AuthenticationException also has a + CredentialsExpiredException and + AccoungtExpiredException subclass, although these + are less commonly used. @@ -1295,6 +1299,23 @@ public aspect DomainObjectInstanceSecurityAspect implements InitializingBean { normally the case when an account is locked. + + AuthenticationFailureAccountExpiredEvent + is published when an authentication request is unsuccessful + because the returned UserDetails indicates the + account has expired. Some applications may wish to distinguish + between an account being disabled and expired. + + + + AuthenticationFailureCredentialsExpiredEvent + is published when an authentication request is unsuccessful + because the returned UserDetails indicates the + account's credentials have expired. Some applications may wish to + expire the credentials if, for example, a password is not changed + with sufficient regularity. + + AuthenticationFailureUsernameNotFoundEvent is published when an authentication request is unsuccessful @@ -3184,8 +3205,8 @@ $CATALINA_HOME/bin/startup.sh As the Authentication object is now in the well-known location, it is handled like any other authentication approach. Usually the - AutoIntegrationFilter will be used to associate - the Authentication object with the + HttpSessionIntegrationFilter will be used to + associate the Authentication object with the ContextHolder for the duration of each request. @@ -4169,8 +4190,8 @@ INSERT INTO acl_permission VALUES (null, 6, 'scott', 1); - Acegi Security System for Spring Auto Integration Filter - (AutoIntegrationFilter) + Acegi Security System for Spring HTTP Session Integration + Filter (HttpSessionIntegrationFilter) diff --git a/doc/xdocs/changes.xml b/doc/xdocs/changes.xml index 5f14d6d068..b6166baf4b 100644 --- a/doc/xdocs/changes.xml +++ b/doc/xdocs/changes.xml @@ -45,6 +45,8 @@ Added AbstractProcessingFilter property to always use defaultTargetUrl Added ContextHolderAwareRequestWrapper to integrate with getRemoteUser() Added attempted username to view if processed by AuthenticationProcessingFilter + Added UserDetails account and credentials expiration methods + Added exceptions and events to support new UserDetails methods Improved BasicAclProvider to only respond to specified ACL object requests Refactored MethodDefinitionSource to work with Method, not MethodInvocation Refactored AbstractFilterInvocationDefinitionSource to work with URL Strings alone @@ -55,6 +57,7 @@ Refactored EH-CACHE implementations to use Spring IoC defined caches instead AbstractProcessingFilter now has various hook methods to assist subclasses DaoAuthenticationProvider better detects AuthenticationDao interface violations + The User class has a new constructor (the old constructor is deprecated) Fixed ambiguous column references in JdbcDaoImpl default query Fixed AbstractProcessingFilter to use removeAttribute (JRun compatibility) Fixed GrantedAuthorityEffectiveAclResolver support of UserDetails principals diff --git a/doc/xdocs/upgrade/upgrade-06-070.html b/doc/xdocs/upgrade/upgrade-06-070.html index 0a7bacba8f..89503cd4f0 100644 --- a/doc/xdocs/upgrade/upgrade-06-070.html +++ b/doc/xdocs/upgrade/upgrade-06-070.html @@ -9,8 +9,15 @@ The following should help most casual users of the project update their applications: