Add event capabilities.

This commit is contained in:
Ben Alex 2004-05-24 00:09:27 +00:00
parent 42ccbfbad7
commit d5c14142d1
11 changed files with 564 additions and 7 deletions

View File

@ -22,11 +22,18 @@ import net.sf.acegisecurity.BadCredentialsException;
import net.sf.acegisecurity.DisabledException; import net.sf.acegisecurity.DisabledException;
import net.sf.acegisecurity.providers.AuthenticationProvider; import net.sf.acegisecurity.providers.AuthenticationProvider;
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; 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.PasswordEncoder;
import net.sf.acegisecurity.providers.encoding.PlaintextPasswordEncoder; import net.sf.acegisecurity.providers.encoding.PlaintextPasswordEncoder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import java.util.Date; import java.util.Date;
@ -56,14 +63,23 @@ import java.util.Date;
* <code>UsernamePasswordAuthenticationToken</code>. This avoids complications * <code>UsernamePasswordAuthenticationToken</code>. This avoids complications
* if the user changes their password during the session. * if the user changes their password during the session.
* </p> * </p>
*
* <P>
* 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.
* </p>
* *
* @author Ben Alex * @author Ben Alex
* @version $Id$ * @version $Id$
*/ */
public class DaoAuthenticationProvider implements AuthenticationProvider, public class DaoAuthenticationProvider implements AuthenticationProvider,
InitializingBean { InitializingBean, ApplicationContextAware {
//~ Instance fields ======================================================== //~ Instance fields ========================================================
private ApplicationContext ctx;
private AuthenticationDao authenticationDao; private AuthenticationDao authenticationDao;
private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder(); private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
private SaltSource saltSource; private SaltSource saltSource;
@ -72,6 +88,11 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
//~ Methods ================================================================ //~ Methods ================================================================
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.ctx = applicationContext;
}
public void setAuthenticationDao(AuthenticationDao authenticationDao) { public void setAuthenticationDao(AuthenticationDao authenticationDao) {
this.authenticationDao = authenticationDao; this.authenticationDao = authenticationDao;
} }
@ -175,6 +196,15 @@ public class DaoAuthenticationProvider implements AuthenticationProvider,
throw new AuthenticationServiceException(repositoryProblem throw new AuthenticationServiceException(repositoryProblem
.getMessage(), 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)) { if (!(authentication instanceof DaoAuthenticationToken)) {
// Must validate credentials, as this is not simply a token refresh // 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(), if (!passwordEncoder.isPasswordValid(user.getPassword(),
authentication.getCredentials().toString(), salt)) { authentication.getCredentials().toString(), salt)) {
if (this.ctx != null) {
ctx.publishEvent(new AuthenticationFailurePasswordEvent(
authentication, user));
}
throw new BadCredentialsException("Bad credentials presented"); throw new BadCredentialsException("Bad credentials presented");
} }
} }
if (!user.isEnabled()) {
throw new DisabledException("User is disabled");
}
Date expiry = new Date(new Date().getTime() Date expiry = new Date(new Date().getTime()
+ this.getRefreshTokenInterval()); + this.getRefreshTokenInterval());
if (this.ctx != null) {
ctx.publishEvent(new AuthenticationSuccessEvent(authentication, user));
}
return new DaoAuthenticationToken(this.getKey(), expiry, return new DaoAuthenticationToken(this.getKey(), expiry,
user.getUsername(), user.getPassword(), user.getAuthorities()); user.getUsername(), user.getPassword(), user.getAuthorities());
} }

View File

@ -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 <code>net.sf.acegisecurity.provider.dao</code> application
* event.
*
* <P>
* 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.
* </p>
*
* <P>
* The <code>ApplicationEvent</code>'s <code>source</code> will be the
* <code>Authentication</code> object.
* </p>
*
* @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 <code>Authentication</code> request that caused the
* event. Also available from <code>super.getSource()</code>.
*
* @return the authentication request
*/
public Authentication getAuthentication() {
return (Authentication) super.getSource();
}
/**
* Getter for the <code>User</code> related to the
* <code>Authentication</code> attempt.
*
* @return the user
*/
public User getUser() {
return user;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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.
*
* <P>
* All authentication failures are logged at the warning level, whilst
* authentication successes are logged at the information level.
* </p>
*
* @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());
}
}
}
}

View File

@ -0,0 +1,9 @@
<html>
<body>
Enables events to be published to the Spring application context.
<P>The <code>DaoAuthenticationProvider</code> automatically publishes
events to the application context. These events are received by all
registered Spring <code>ApplicationListener</code>s.</P>
</body>
</html>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -7,7 +7,7 @@
<subtitle>Reference Documentation</subtitle> <subtitle>Reference Documentation</subtitle>
<releaseinfo>0.5</releaseinfo> <releaseinfo>0.51</releaseinfo>
<authorgroup> <authorgroup>
<author> <author>
@ -76,7 +76,9 @@
<listitem> <listitem>
<para>An <literal>Authentication</literal> object which holds the <para>An <literal>Authentication</literal> object which holds the
principal, credentials and the authorities granted to the principal, credentials and the authorities granted to the
principal.</para> principal. The object can also store additional information
associated with an authentication request, such as the source
TCP/IP address.</para>
</listitem> </listitem>
<listitem> <listitem>
@ -952,6 +954,63 @@
desired.</para> desired.</para>
</sect2> </sect2>
<sect2 id="security-authentication-provider-in-memory">
<title>Event Publishing</title>
<para>The <literal>DaoAuthenticationProvider</literal> automatically
obtains the <literal>ApplicationContext</literal> 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:</para>
<itemizedlist spacing="compact">
<listitem>
<para><literal>AuthenticationSuccessEvent</literal> is published
when an authentication request is successful.</para>
</listitem>
<listitem>
<para><literal>AuthenticationFailureDisabledEvent</literal> is
published when an authentication request is unsuccessful because
the returned <literal>User</literal> is disabled. This is normally
the case when an account is locked.</para>
</listitem>
<listitem>
<para><literal>AuthenticationFailurePasswordEvent</literal> is
published when an authentication request is unsuccessful because
the presented password did not match that in the
<literal>User</literal>.</para>
</listitem>
</itemizedlist>
<para>Each event contains two objects: the
<literal>Authentication</literal> object that represented the
authentication request, and the <literal>User</literal> object that
was found in response to the authentication request. The
<literal>Authentication</literal> interface provides a
<literal>getDetails()</literal> method which often includes
information that event consumers may find useful (eg the TCP/IP
address that the authentication request originated from).</para>
<para>As per standard Spring event handling, you can receive these
events by adding a bean to the application context which implements
the <literal>ApplicationListener</literal> interface. Included with
Acegi Security is a <literal>LoggerListener</literal> class which
receives these events and publishes their details to Commons Logging.
Refer to the JavaDocs for <literal>LoggerListener</literal> for
details on the logging priorities used for different message
types.</para>
<para>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.</para>
</sect2>
<sect2 id="security-authentication-provider-in-memory"> <sect2 id="security-authentication-provider-in-memory">
<title>In-Memory Authentication</title> <title>In-Memory Authentication</title>

View File

@ -46,6 +46,9 @@
<property name="key"><value>my_password</value></property> <property name="key"><value>my_password</value></property>
</bean> </bean>
<!-- Automatically receives AuthenticationEvent messages from DaoAuthenticationProvider -->
<bean id="loggerListener" class="net.sf.acegisecurity.providers.dao.event.LoggerListener"/>
<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"> <bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property> <property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property> <property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>