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 947cb61f37..288ce23910 100644 --- a/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java +++ b/core/src/main/java/org/acegisecurity/providers/dao/DaoAuthenticationProvider.java @@ -22,11 +22,18 @@ import net.sf.acegisecurity.BadCredentialsException; import net.sf.acegisecurity.DisabledException; import net.sf.acegisecurity.providers.AuthenticationProvider; import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationFailurePasswordEvent; +import net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent; import net.sf.acegisecurity.providers.encoding.PasswordEncoder; import net.sf.acegisecurity.providers.encoding.PlaintextPasswordEncoder; +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; import java.util.Date; @@ -56,14 +63,23 @@ import java.util.Date; * UsernamePasswordAuthenticationToken. This avoids complications * if the user changes their password during the session. *

+ * + *

+ * 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 Ben Alex * @version $Id$ */ public class DaoAuthenticationProvider implements AuthenticationProvider, - InitializingBean { + InitializingBean, ApplicationContextAware { //~ Instance fields ======================================================== + private ApplicationContext ctx; private AuthenticationDao authenticationDao; private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder(); private SaltSource saltSource; @@ -72,6 +88,11 @@ public class DaoAuthenticationProvider implements AuthenticationProvider, //~ Methods ================================================================ + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.ctx = applicationContext; + } + public void setAuthenticationDao(AuthenticationDao authenticationDao) { this.authenticationDao = authenticationDao; } @@ -175,6 +196,15 @@ public class DaoAuthenticationProvider implements AuthenticationProvider, throw new AuthenticationServiceException(repositoryProblem .getMessage(), repositoryProblem); } + + if (!user.isEnabled()) { + if (this.ctx != null) { + ctx.publishEvent(new AuthenticationFailureDisabledEvent( + authentication, user)); + } + + throw new DisabledException("User is disabled"); + } if (!(authentication instanceof DaoAuthenticationToken)) { // Must validate credentials, as this is not simply a token refresh @@ -186,17 +216,22 @@ public class DaoAuthenticationProvider implements AuthenticationProvider, if (!passwordEncoder.isPasswordValid(user.getPassword(), authentication.getCredentials().toString(), salt)) { + if (this.ctx != null) { + ctx.publishEvent(new AuthenticationFailurePasswordEvent( + authentication, user)); + } + throw new BadCredentialsException("Bad credentials presented"); } } - if (!user.isEnabled()) { - throw new DisabledException("User is disabled"); - } - Date expiry = new Date(new Date().getTime() + this.getRefreshTokenInterval()); + if (this.ctx != null) { + ctx.publishEvent(new AuthenticationSuccessEvent(authentication, user)); + } + return new DaoAuthenticationToken(this.getKey(), expiry, user.getUsername(), user.getPassword(), user.getAuthorities()); } diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationEvent.java new file mode 100644 index 0000000000..2fd5fc28c9 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationEvent.java @@ -0,0 +1,83 @@ +/* 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.providers.dao.User; + +import org.springframework.context.ApplicationEvent; + + +/** + * Represents a net.sf.acegisecurity.provider.dao application + * event. + * + *

+ * Subclasses exist for different types of authentication events. All + * authentication events relate to a particular {@link User} and are caused by + * a particular {@link Authentication} object. This is intended to permit + * logging of successful and unsuccessful login attempts, and facilitate the + * locking of accounts. + *

+ * + *

+ * The ApplicationEvent's source will be the + * Authentication object. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public abstract class AuthenticationEvent extends ApplicationEvent { + //~ Instance fields ======================================================== + + private User user; + + //~ Constructors =========================================================== + + public AuthenticationEvent(Authentication authentication, User user) { + super(authentication); + + // No need to check authentication isn't null, as done by super + if (user == null) { + throw new IllegalArgumentException("User is required"); + } + + this.user = user; + } + + //~ Methods ================================================================ + + /** + * Getters for the Authentication request that caused the + * event. Also available from super.getSource(). + * + * @return the authentication request + */ + public Authentication getAuthentication() { + return (Authentication) super.getSource(); + } + + /** + * Getter for the User related to the + * Authentication attempt. + * + * @return the user + */ + public User getUser() { + return user; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureDisabledEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureDisabledEvent.java new file mode 100644 index 0000000000..7fda5fa7b8 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailureDisabledEvent.java @@ -0,0 +1,36 @@ +/* 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.providers.dao.User; + + +/** + * Application event which indicates authentication failure due to the user's + * account being locked. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationFailureDisabledEvent extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationFailureDisabledEvent(Authentication authentication, + User user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailurePasswordEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailurePasswordEvent.java new file mode 100644 index 0000000000..ec65c5a212 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationFailurePasswordEvent.java @@ -0,0 +1,36 @@ +/* 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.providers.dao.User; + + +/** + * Application event which indicates authentication failure due to invalid + * password. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationFailurePasswordEvent extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationFailurePasswordEvent(Authentication authentication, + User user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationSuccessEvent.java b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationSuccessEvent.java new file mode 100644 index 0000000000..d379778361 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/AuthenticationSuccessEvent.java @@ -0,0 +1,34 @@ +/* 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.providers.dao.User; + + +/** + * Application event which indicates successful authentication. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationSuccessEvent extends AuthenticationEvent { + //~ Constructors =========================================================== + + public AuthenticationSuccessEvent(Authentication authentication, User user) { + super(authentication, user); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java b/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java new file mode 100644 index 0000000000..ec386415ec --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/LoggerListener.java @@ -0,0 +1,75 @@ +/* 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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + + +/** + * Outputs authentication-related application events to Commons Logging. + * + *

+ * All authentication failures are logged at the warning level, whilst + * authentication successes are logged at the information level. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class LoggerListener implements ApplicationListener { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(LoggerListener.class); + + //~ Methods ================================================================ + + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof AuthenticationFailurePasswordEvent) { + AuthenticationFailurePasswordEvent authEvent = (AuthenticationFailurePasswordEvent) event; + + if (logger.isWarnEnabled()) { + logger.warn("Authentication failed due to incorrect password for user: " + + authEvent.getUser().getUsername() + "; details: " + + authEvent.getAuthentication().getDetails()); + } + } + + if (event instanceof AuthenticationFailureDisabledEvent) { + AuthenticationFailureDisabledEvent authEvent = (AuthenticationFailureDisabledEvent) event; + + if (logger.isWarnEnabled()) { + logger.warn( + "Authentication failed due to account being disabled for user: " + + authEvent.getUser().getUsername() + "; details: " + + authEvent.getAuthentication().getDetails()); + } + } + + if (event instanceof AuthenticationSuccessEvent) { + AuthenticationSuccessEvent authEvent = (AuthenticationSuccessEvent) event; + + if (logger.isInfoEnabled()) { + logger.info("Authentication success for user: " + + authEvent.getUser().getUsername() + "; details: " + + authEvent.getAuthentication().getDetails()); + } + } + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/dao/event/package.html b/core/src/main/java/org/acegisecurity/providers/dao/event/package.html new file mode 100644 index 0000000000..07dac2ec55 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/dao/event/package.html @@ -0,0 +1,9 @@ + + +Enables events to be published to the Spring application context. + +

The DaoAuthenticationProvider automatically publishes +events to the application context. These events are received by all +registered Spring ApplicationListeners.

+ + 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 new file mode 100644 index 0000000000..e8963c76df --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/dao/event/AuthenticationEventTests.java @@ -0,0 +1,105 @@ +/* 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 junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.User; + + +/** + * Tests {@link AuthenticationEvent} and its subclasses. + * + * @author Ben Alex + * @version $Id$ + */ +public class AuthenticationEventTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(AuthenticationEventTests.class); + } + + public void testDisabledEvent() { + Authentication auth = getAuthentication(); + User user = getUser(); + AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(auth, + user); + assertEquals(auth, event.getAuthentication()); + assertEquals(user, event.getUser()); + } + + public void testPasswordEvent() { + Authentication auth = getAuthentication(); + User user = getUser(); + AuthenticationFailurePasswordEvent event = new AuthenticationFailurePasswordEvent(auth, + user); + assertEquals(auth, event.getAuthentication()); + assertEquals(user, event.getUser()); + } + + public void testRejectsNullAuthentication() { + try { + AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(null, + getUser()); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testRejectsNullUser() { + try { + AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(getAuthentication(), + null); + fail("Should have thrown IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testSuccessEvent() { + Authentication auth = getAuthentication(); + User user = getUser(); + AuthenticationSuccessEvent event = new AuthenticationSuccessEvent(auth, + user); + assertEquals(auth, event.getAuthentication()); + assertEquals(user, event.getUser()); + } + + private Authentication getAuthentication() { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal", + "Credentials"); + authentication.setDetails("127.0.0.1"); + + return authentication; + } + + private User getUser() { + User user = new User("foo", "bar", 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 new file mode 100644 index 0000000000..e45033caaa --- /dev/null +++ b/core/src/test/java/org/acegisecurity/providers/dao/event/LoggerListenerTests.java @@ -0,0 +1,82 @@ +/* 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 junit.framework.TestCase; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.User; + + +/** + * Tests {@link LoggerListener}. + * + * @author Ben Alex + * @version $Id$ + */ +public class LoggerListenerTests extends TestCase { + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(LoggerListenerTests.class); + } + + public void testLogsDisabledEvents() { + AuthenticationFailureDisabledEvent event = new AuthenticationFailureDisabledEvent(getAuthentication(), + getUser()); + LoggerListener listener = new LoggerListener(); + listener.onApplicationEvent(event); + assertTrue(true); + } + + public void testLogsPasswordEvents() { + AuthenticationFailurePasswordEvent event = new AuthenticationFailurePasswordEvent(getAuthentication(), + getUser()); + LoggerListener listener = new LoggerListener(); + listener.onApplicationEvent(event); + assertTrue(true); + } + + public void testLogsSuccessEvents() { + AuthenticationSuccessEvent event = new AuthenticationSuccessEvent(getAuthentication(), + getUser()); + LoggerListener listener = new LoggerListener(); + listener.onApplicationEvent(event); + assertTrue(true); + } + + private Authentication getAuthentication() { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("Principal", + "Credentials"); + authentication.setDetails("127.0.0.1"); + + return authentication; + } + + private User getUser() { + User user = new User("foo", "bar", true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_FOOBAR")}); + + return user; + } +} diff --git a/docs/reference/src/index.xml b/docs/reference/src/index.xml index 8cfb626232..9ffb267c9b 100644 --- a/docs/reference/src/index.xml +++ b/docs/reference/src/index.xml @@ -7,7 +7,7 @@ Reference Documentation - 0.5 + 0.51 @@ -76,7 +76,9 @@ An Authentication object which holds the principal, credentials and the authorities granted to the - principal. + principal. The object can also store additional information + associated with an authentication request, such as the source + TCP/IP address. @@ -952,6 +954,63 @@ desired. + + Event Publishing + + The DaoAuthenticationProvider automatically + obtains the ApplicationContext it is running in at + startup time. This allows the provider to publish events through the + standard Spring event framework. Three types of event messages are + published: + + + + AuthenticationSuccessEvent is published + when an authentication request is successful. + + + + AuthenticationFailureDisabledEvent is + published when an authentication request is unsuccessful because + the returned User is disabled. This is normally + the case when an account is locked. + + + + AuthenticationFailurePasswordEvent is + published when an authentication request is unsuccessful because + the presented password did not match that in the + User. + + + + Each event contains two objects: the + Authentication object that represented the + authentication request, and the User object that + was found in response to the authentication request. The + Authentication interface provides a + getDetails() method which often includes + information that event consumers may find useful (eg the TCP/IP + address that the authentication request originated from). + + As per standard Spring event handling, you can receive these + events by adding a bean to the application context which implements + the ApplicationListener interface. Included with + Acegi Security is a LoggerListener class which + receives these events and publishes their details to Commons Logging. + Refer to the JavaDocs for LoggerListener for + details on the logging priorities used for different message + types. + + This event publishing system enables you to implement account + locking and record authentication event history. This might be of + interest to application users, who can be advised of the times and + source IP address of all unsuccessful password attempts (and account + lockouts) since their last successful login. Such capabilities are + simple to implement and greatly improve the security of your + application. + + In-Memory Authentication diff --git a/samples/contacts/etc/filter/applicationContext.xml b/samples/contacts/etc/filter/applicationContext.xml index 46193e6980..ffbce7886e 100644 --- a/samples/contacts/etc/filter/applicationContext.xml +++ b/samples/contacts/etc/filter/applicationContext.xml @@ -46,6 +46,9 @@ my_password + + +