Acegi Concurrent Login

It is a security requirement for most web sites to disable concurrent logins, so users cannot login from different machines using same login details.

Let’s see how to enable this functionality with Acegi Security.

Firstly, add org.acegisecurity.concurrent.SessionRegistry implementation bean to your security context:

<bean id="sessionRegistry" class="org.acegisecurity.concurrent.SessionRegistryImpl" />

We are using default Acegi implementation org.acegisecurity.concurrent.SessionRegistryImpl.

Next, define the org.acegisecurity.concurrent.SessionController bean:


    <bean id="sessionController" class="org.acegisecurity.
           concurrent.ConcurrentSessionControllerImpl">
        <property name="exceptionIfMaximumExceeded" value="true"/>
        <property name="maximumSessions" value="1" />
        <property name="sessionRegistry" ref="sessionRegistry"/>
    </bean>

As you can see, it takes sessionRegistry property, as well as two additional properties maximumSessions and exceptionIfMAximumExceeded.
maximumSessions says how meny concurrent login sessions are allowed (in our case just one)
if exceptionIfMAximumExceeded property is set to true, exception will be thrown every time the user tries to login concurrently. You can check this exception in your login controller and display user with a message.
Otherwise, if exceptionIfMAximumExceeded property is set to false, exception will NOT be thrown. If user tries to login concurrently, he will be allowed, but his last login session (before the concurrent one) will be invalidated.

Last step is to add sessionController property to your ProviderManager bean:

     <bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
		<property name="providers">
			<list>
				<ref local="daoAuthenticationProvider"/>
			</list>
		</property>
        <property name="sessionController" ref="sessionController"/>
    </bean>

And you’re ready to run.

Some users have encountered problems with concurrent logins: If a user logs out, and then tries to log in again, the ConcurrentLoginException is thrown, so user cannot log in again. This happens when Acegi logout does not remove the session data for the user that has been logout out (before his login session has expired)
In order to fix this, you can manually clear the authentication session for the user that’s logged out:

public void logout() {
        SecurityContext context = SecurityContextHolder.getContext();
        if (context == null) return;
        Authentication authentication = context.getAuthentication();
        if (authentication == null) return;
        String sessionId = SessionRegistryUtils.obtainSessionIdFromAuthentication(authentication);
        this.sessionRegistry.removeSessionInformation(sessionId);
}


You will also need this code to be run when Acegi session gets unpublished.
For this implement org.acegisecurity.ui.session.HttpSessionEventPublisher, and configure listener for it in your web.xml:

public class MyHttpSessionEventPublisher extends HttpSessionEventPublisher {
    private static final Log logger = LogFactory.getLog(MyHttpSessionEventPublisher.class);
    private UserContext userContext;

    public void sessionDestroyed(HttpSessionEvent event) {
        logger.info("unpublishing session");
        if (userContext == null) {
            this.userContext = lookupBean(
                        WebApplicationContextUtils.
                             getWebApplicationContext(
                              event.getSession().getServletContext()),
                       "userContext",
                       UserContext.class);
        }

        this.userContext.invalidate();
        super.sessionDestroyed(event);
    }

    private  T lookupBean(final ApplicationContext applicationContext, final String beanName, final Class c) {
        //noinspection unchecked
        return (T) applicationContext.getBean(beanName, c);
    }
}

In web.xml you will have:

<listener>
        <listener-class>net.cakesolutions.service.security.acegi.BimHttpSessionEventPublisher</listener-class>
</listener>


And you’re ready to go.

Hope this article has helped anyone in configuring concurrent logins with Acegi Security.

Tags: , , , ,

2 Responses to “Acegi Concurrent Login”

  1. Lady Raveneve Says:

    Hi ! I have tried to implement your MyHttpSessionEventPublisher but am having some problems with the declaration of UserContext. May I know which package this class is suppose to be from? Am i suppose to implement my own UserContext?

    Your help is greatly appreciated as my app intermittently throws ConcurrentLoginException even though the user has logged out of the system.

    I am using JDK1.4 and acegi-security 1..0.4. Thanks !

  2. Aleksa Vukotic Says:

    Yes, you should have your own UserContext implementation.
    Following implementation has only two methods, User getUser(), which gets the user currently logged in, and void logout(), which destroys the session for user currently logged in.
    Here is the UserContext interface, and sample implementation:

    public interface UserContext {

    /**
    * Gets the current user
    * @return The User object identifying the user
    */
    User getUser();

    /**
    * Performs logout for current user
    */
    void logout();
    }


    public class AcegiUserContext implements UserContext, InitializingBean {
    private static final Log logger = LogFactory.getLog(AcegiUserDetails.class);

    private UserService userService;
    private SessionRegistry sessionRegistry;
    public User getUser() {
    SecurityContext context = SecurityContextHolder.getContext();
    if (context == null) return null;
    Authentication authentication = context.getAuthentication();
    if (authentication == null) return null;

    String username = authentication.getPrincipal().toString();

    if (authentication.getPrincipal() instanceof UserDetails) {
    username = ((UserDetails) authentication.getPrincipal()).getUsername();
    }

    return this.userService.findByUsername(username);
    }

    public void logout() {
    SecurityContext context = SecurityContextHolder.getContext();
    if (context == null) return;
    Authentication authentication = context.getAuthentication();
    if (authentication == null) return;
    String sessionId = SessionRegistryUtils.obtainSessionIdFromAuthentication(authentication);
    this.sessionRegistry.removeSessionInformation(sessionId);

    }

    public void afterPropertiesSet() throws Exception {
    if (this.userService == null) throw new FatalBeanException("Property [userService] of [" + getClass().getName() + "] is required.”);
    if (this.sessionRegistry == null) throw new FatalBeanException(”Property [sessionRegistry] of [" + getClass().getName() + "] is required.”);
    }

    public void setUserService(UserService userService) {
    this.userService = userService;
    }

    public void setSessionRegistry(SessionRegistry sessionRegistry) {
    this.sessionRegistry = sessionRegistry;
    }
    }

    Finally, you should create bean definition for userContext bean:

    <bean id="userContext" class="net.cakesolutions.service.security.acegi.AcegiUserContext">
    <property name="userService" ref="userService"/>
    <property name="sessionRegistry" ref="sessionRegistry" />
    </bean>

    SessionRegistry is the the reference to the implementation shown in the blog, and the userService is s standard service for use management.

    .

    Hope this helps, let us know how are you getting on.

Leave a Reply