From the desk of a gadget lover
RSS icon Home icon
  • Spring (Acegi) Security Account Lockout

    Posted on February 28th, 2010 Hari Gangadharan 15 comments

    Due to my previous articles in Spring Security, some has asked me how to implement the Account Lockout on too many failed login attempts. The best way to do this is to listen for Spring events and update your User object on a failed login.  Let us start with your User object. It must be implementing the UserDetails interface and you might have already noticed that it has a method isAccountNonLocked(). You might be currently returning true in the implementation. Now we have to change the logic here to implement locked status.

    // User.java
    public class User implements Serializable,
         org.springframework.security.userdetails.UserDetails {
        // let us put max failed login attempts at 5
        public static final short MAX_FAILED_LOGIN_ATTEMPTS = 5;
    
        /**
         * An attribute to track the number of failed login attempts
         */
        private int failedLoginAttempts;
    
        // all other attributes and the getters and setters.
    
        /**
         * Implementation for the UserDetails interface. Verifies that
         * the account is not locked. Returns true if account is not
         * locked. Otherwise returns false.
         */
        public boolean isAccountNonLocked() {
            if (this.getFailedLoginAttempts()
                       >= MAX_FAILED_LOGIN_ATTEMPTS) {
                return false;
            }
            return true;
        }
    }
    

    That takes care of the actual locking of the account. Now we have to increment the failedLoginAttempts every time an user enters the wrong credentials. Luckily, Spring Security creates an AuthenticationFailureBadCredentialsEvent every time a user tries to login with wrong credentials. Now we have to implement an ApplicationListener to listen for this event. This class shall implement the Spring ApplicationListener interface. Actually you need only one application listener for your whole application:

    public class ApplicationEventListener implements
        org.springframework.context.ApplicationListener {
        public void onApplicationEvent(ApplicationEvent event) {
            if (event instanceof AuthenticationFailureBadCredentialsEvent){
                // do everything for bad login
                // (increment failedLoginAttempts)
            else if (event instanceof SomeOtherEvent) {
                // handle the "some other event"!
            }
        }
    }
    

    However somebody with a little bit of Object Oriented Programming knowledge can tell that this is a bad design. The reason is this class handles too many events and this class has to be modified whenever your application has to listen for a new event. Hence we will refactor the code in an Object Oriented way. We will modify this class to listen for the application events and to dispatch it to an appropriate listener. We will then create one listener for every Event we have to handle. To begin, an abstract class EventListener is created. All event listeners extend this class and implement the abstract methods. This could have been an interface but it has been created as an abstract class so that I can add the code to automatically register the listener with the main application event dispatcher.

    // EventListener.java
    package wisdom.web.event;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    
    /**
     * An abstract class that is the parent for event listeners
     * in this application. Any event listener should extend
     * this class.
     *
     * @author Hari Gangadharan
     */
    public abstract class EventListener implements InitializingBean {
    
        Log log = LogFactory.getLog(this.getClass());
    
       // the instance of the event dispatcher will be
       // auto-wired by spring
       @Autowired
       EventDispatcher eventDispatcher;
    
       // Spring will call this method after auto-
       // wiring is complete.
       public void afterPropertiesSet() throws Exception {
           // let us register this instance with
           // event dispatcher
           eventDispatcher.registerListener(this);
       }
    
       /**
        * Implementation of this method checks whether the given event can
        * be handled in this class. This method will be called by the event
        * dispatcher.
        *
        * @param event the event to handle
        * @return true if the implementing subclass can handle the event
        */
       public abstract boolean canHandle(Object event);
    
       /**
        * This method is executed by the event dispatcher with the
        * event object.
        *
        * @param event the event to handle
        */
       public abstract void handle(Object event);
    }
    

    Here you will see that this bean auto-wires the EventDispatcher (which we have not written yet). Also we register this instance to the EventDispatcher after the properties are set. Now we will write an Event Dispatcher that receives Spring Events and dispatches it to the appropriate Listener.

    // EventDispatcher.java
    package wisdom.web.event;
    
    import java.util.ArrayList;
    import java.util.List;
    import org.springframework.context.ApplicationEvent;
    import org.springframework.stereotype.Component;
    
    /**
     * This class implements the Spring ApplicationListener interface and
     * hence it receives application event notifications. This in turn
     * dispatches the events to listeners that have registered with
     * this object.
     *
     * @author Hari Gangadharan
     */
    @Component("eventDispatcher")
    public class EventDispatcher implements
         org.springframework.context.ApplicationListener {
    
        List<EventListener> listeners = new ArrayList<EventListener>();
    
        /**
         * Method that allows registering of an Event Listener.
         */
        public void registerListener(EventListener listener) {
            listeners.add(listener);
        }
    
        /**
         * Spring executes this method with the event object.
         * This method iterates though the list of registered
         * Listeners and checks whether any listener can
         * handle the event. Calls handle method of the
         * Listener if it can handle the event.
         */
        public void onApplicationEvent(ApplicationEvent event) {
            for (EventListener listener: listeners) {
                if (listener.canHandle(event)) {
                    listener.handle(event);
                }
            }
        }
    

    Now we are ready to write the code that will listen for the login failures. As you know this code will increment the failedLoginAttempt of User object.

    // LoginFailureEventListener.java
    package wisdom.web.event;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.event.authentication
          .AuthenticationFailureBadCredentialsEvent;
    import org.springframework.stereotype.Component;
    import wisdom.api.model.User;
    import wisdom.api.service.UserManager;
    
    /**
     * A listener that listens for the Login Failure Event. This listener
     * updates the failed login attempt count which in turn locks out
     * the user.
     *
     * @author Hari Gangadharan
     */
    @Component("loginFailureEventListener")
    public class LoginFailureEventListener extends EventListener {
        // this is your User Service. We call a method
        // in this to update the User object.
        @Autowired
        UserManager userManager;
    
        @Override
        public boolean canHandle(Object event) {
            return event instanceof
                 AuthenticationFailureBadCredentialsEvent;
        }
    
        @Override
        public void handle(Object event) {
            AuthenticationFailureBadCredentialsEvent loginFailureEvent
                 = (AuthenticationFailureBadCredentialsEvent) event;
            Object name = loginFailureEvent.getAuthentication()
                      .getPrincipal();
            User user = userManager.getUser((String) name);
            if (user != null) {
                // update the failed login count
                short failedLoginAttempts = user.getFailedLoginAttempts();
                user.setFailedLoginAttempts(++failedLoginAttempts);
                // update user
                userManager.updateUser(user);
            }
        }
    }
    

    We are almost there… Now the we have to do two more things. Can you guess? The first one is to create another listener for successful authentication event. If the user is successful in logging in and if the failedLoginAttempts count is greater than 0 then we have reset the count (to 0). Otherwise the failedLoginAttempts may get accumulated over time with occasional login failures and finally the account may become locked. The second thing to do is to alert the user if the account is locked. Let us do the first thing viz. the listener now:

    // LoginSuccessEventListener.java
    package wisdom.web.event;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.event.authorization.AuthorizedEvent;
    import org.springframework.stereotype.Component;
    import wisdom.api.model.User;
    import wisdom.api.service.UserManager;
    
    /**
     * Listens for the Login Success.
     * This class resets the failed login count.
     *
     * @author Hari Gangadharan
     */
    @Component("loginSuccessEventListener")
    public class LoginSuccessEventListener extends EventListener {
    
        @Autowired
        UserManager userManager;
        /**
         * A private method that gets the User object
         * from the event if it is an AuthorizedEvent.
         * Otherwise returns null.
         */
        private User getPrincipal(Object event) {
            if (event instanceof AuthorizedEvent) {
                AuthorizedEvent authorizedEvent = (AuthorizedEvent) event;
                Object principal = authorizedEvent.getAuthentication()
                         .getPrincipal();
                if (principal instanceof User) {
                    return (User) principal;
                }
            }
            return null;
        }
    
        @Override
        public boolean canHandle(Object event) {
            User principal = this.getPrincipal(event);
            return (principal != null);
        }
    
        @Override
        public void handle(Object event) {
            User user = this.getPrincipal(event);
            try {
                if (user.getFailedLoginAttempts() > 0) {
                    // reset failed login count to zero
                    // on a successful login
                    user.setFailedLoginAttempts((short) 0);
                    // update user
                    userManager.updateUser(user);
                }
            } catch (Exception ex) {
                // just log the exception
                log.error("Failure in login success event handling", ex);
            }
        }
    }
    

    That completes the listener. Now we have to do the last thing – alert the user that the account is locked and direct the user to further actions like contacting customer care or verifying account. Depending on the framework used, you may have to figure out where the code is to be added:

    ExternalContext externalContext = FacesUtils.getExternalContext();
    Exception e = (Exception) externalContext.getSessionMap().get(
             AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY);
    // check if there is a Bad Cred Exception
    if (e instanceof BadCredentialsException) {
        // add code to show the Login Invalid Error Message
    // check if it is Account Locked
    } else if (e instanceof LockedException) {
        // add code to show Account Locked Error Message
    }
    // reset the last exception key
    externalContext.getSessionMap().put(
         AbstractProcessingFilter.SPRING_SECURITY_LAST_EXCEPTION_KEY, null);
    

    If the framework used is JSF the above code can go into the Phase Listener or on the the form binding variable setter for the login form.

    Share this page:
    • Digg
    • del.icio.us
    • Facebook
    • Google Bookmarks
    • StumbleUpon
    • Technorati
    • TwitThis
     

    15 responses to “Spring (Acegi) Security Account Lockout”

    1. Hi.
      Thanks for posting a solution to implement the custom listener. The question is where do I register these listeners? Reply is appreciated.

      Thanks.

    2. Hari Gangadharan

      I use annotations to register the Spring beans. If you don’t use annotations, you may have to define these beans in spring context XML. No separate registration of listeners is needed. It is all auto-wired and initialized by Spring… And Spring automatically registers the ApplicationEventListener since it implements ApplicationListener interface.

    3. Hi Gangadhar,
      Your post is very very helpful and very well explained also.
      I have just started working on acegi security. I’m working on an existing solution which is built on acegi-security 1.0.3.
      Also I’m new to Spring.

      My application is a web application which has a acegi-security.xml defined and placed in the WEB-INF folder.
      Can you please let me know how to add the above listeners in the above mentioned xml without using annotations and autowiring.
      Your inputs are highly appreciated.

      Thanks and regards,
      Chalapathi

    4. @Chalapathi: It is been years since I have coded context xml. Even in the days when the annotations were not there we used fake annotations in comments and used xdoclets to generate the context xml. First, you have to add all the Java classes without the annotations. Then find all classes that had @Component Annotation and define them in context xml (find the file with all these bean definitions):
      instead of @Component(”eventDispatcher”) you have to do the bean definition in the context xml:
      <bean id=”eventDispatcher” class=”your.package.EventDispatcher” />

      For beans that have attributes with @Autowired, you have to define property injection in the bean definition – example:
      <bean id=”loginFailureEventListener” class=”your.package.LoginFailureEventListener” >
      <property name=”userManager” ref=”yourUserServiceBeanId” />
      (You have to also add the @Autowired properties of the parent too – parent of this bean is EventListener and that has @Autowired for eventDispatcher attribute)
      <property name=”eventDispatcher” ref=”eventDispatcher” />
      <bean >

      Hope this helps.

    5. @Hari Gangadharan

      Great explanation and examples. I implemented your good and get it to work without the autowire and a couple of adjustments.

      Now After 3 wrong login attempts spring change the value of accountNonlocked from 1 to 0 and failedLoginAttempts stand on 3. I think this is correct. But when I filled in a good Login, the user isn’t blocked. The LoginSuccessEventlistener don’t even come in the function isAccountNonLocked() so he never checked if the account is locked.

      I use Spring 2.5.6 en Spring-security 2.0 Do you have some experience with this? seems like a bug to me….

    6. Good job :)

    7. ….. but as of Spring 3.0, new ApplicationListener can be declared and registered in AppContext to automatically filter events of exact type

    8. Hari Gangadharan

      @Michel
      Interesting. Normally it should throw a LockedException.

    9. Great explanation , But i am confused where to put last code in your article to show error message

      Thanks

    10. Great stuff. Exactly what I needed. I am adapting for Spring Security 3.

    11. Hari Gangadharan

      It depends on what type of framework you are using… If you are using JSF, the easiest thing is to add a binding variable:

      For example:

      <h:form binding="#{action.form}.....

      and in your action:

      private UIComponent form;
      pubic void setForm(UIComponent form) {
      this.form = form;
      // rest of the code in last section goes here
      }

    12. It’s authentication.event, not event.authentication

    13. hi Hari,
      could you please provide whole sourcecode for this example?

    14. Great post Hari but am facing an issue here:
      In LoginSuccessEventListener.java

      @Override
      public boolean canHandle(Object event) {
      User principal = this.getPrincipal(event);
      return (principal != null);
      }

      Here principal will be null when the server starts so this event is not registered.

      Thanks in advance :-)

    15. Nice Job Hari!

    Leave a reply