Restricting concurrent user sessions in grails 2 using spring-security-core plugin

A common requirement in web application development is to restrict multiple concurrent user logins. In other words, a person who subscribed to your shiny new web app using the cheap single-user-subscription should not be able to login from several different computers at the same time.

Let’s see who we can implement that functionality using Grails 2.0 and Spring Security Core Plugin (v1.2.7.1, which uses Spring Security 3.0.7 under the hood). While digging for extisting solutions I cam across several slightly outdated posts on the grais mailing lists (post1 post2 post3), an unanswered question on stackoverflow, as well as the excellent Spring Security 3 and Grails Spring Security Plugin documentations.

Based on all this I worked my way towards the solution described below.

First we need to enable the HttpSessionEventPublisher in order to participate in session events. Simply add

grails.plugins.springsecurity.useHttpSessionEventPublisher = true

to Config.groovy and spring security plugin takes care of the rest. As soon as the listener is registered an ApplicationEvent will be published to Spring ApplicationContext each time a HttpSession commences or ends. This event can then be consumed by a ConcurrentSessionFilter as described below.

Then, in resources.groovy, we define a sessionRegistry bean (refering to the default SessionRegistryImpl provided by Spring Security), a concurrencyFilter bean, and a concurrentSessionControlStrategy bean.

import org.springframework.security.core.session.SessionRegistryImpl
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy
import org.springframework.security.web.session.ConcurrentSessionFilter

beans = {
  sessionRegistry(SessionRegistryImpl)

  concurrencyFilter(ConcurrentSessionFilter) {
    sessionRegistry = sessionRegistry
    logoutHandlers = [ref("rememberMeServices"), ref("securityContextLogoutHandler")]
    expiredUrl='/login/concurrentSession'
  }
  concurrentSessionControlStrategy(ConcurrentSessionControlStrategy, sessionRegistry) {
    alwaysCreateSession = true
    exceptionIfMaximumExceeded = false
    maximumSessions = 1
  }
  // Better not to override the bean but to just set the strategy in Bootstrap.groovy (Details  below)
  /*
  authenticationProcessingFilter(UsernamePasswordAuthenticationFilter) {
    sessionAuthenticationStrategy=concurrentSessionControlStrategy
    authenticationManager=ref("authenticationManager")
  }
  */
}

The concurrentSessionControl strategy allows us to (1) define the maximum permitted number of concurrent sessions a user is allowed to have, and (2) the consequences if this number is exceeded by adjusting exceptionIfMaximumExceeded. It this property is set to false, old sessions of a user are quietly invalidated as soon as he creates a new session (i.e. he logs in on another computer). Otherwise the new login attempt would fail with an exception as long as other valid sessions exist.

The concurrencyFilter does two things. (1) Update sessionRegistry so that the registered session’s “last update” time is always correct, and (2) Check if the session is marked as expired and – if that is the case – call the configured logoutHandlers.

Note: We explicitly configure [ref("rememberMeServices"), ref("securityContextLogoutHandler")] as logoutHandlers because by default Spring Security would inject only the securityContextLogoutHandler into the filter.
Why is that important? If we leave the defaults untouched the following can happen:
User Alice logs in on computer A with “remember-me”-box on the default login page checked. Afterwards Alice also logs in on computer B using the same credentials as before. Upon login on computer B our securityContextLogoutHandler invalidates Alice’s session for computer A and then lets her proceed. If Alice now returns to computer A her session is invalid so that the system triggers a new login. But as it finds a valid remember-me cookie it automatically re-authenticates alice and lets her proceed on Computer A too as long as the cookie remains valid. Clearly not what we wanted to achieve… So always remember to register rememberMeServices as logout handler too whenever your system uses cookie based authentication.

Finally, we override authenticationProcessingFilter (which ships readily configured with spring security core plugin) so that it uses our custom sessionAuthenticationStrategy.
Note that we use the name authenticationProcessingFilter although in Spring Security 3 this class was renamed into UsernamePasswordAuthenticationFilter. But as the spring-security-core plugin (v1.2.7.1) still uses the old name in its default filter chain and as we want to override that particular bean with our new configuration we stick to the old bean name.

Update: Overwriting this bean would break some of the Spring Security Plugin config options. A better approach is to just set the sessionAuthenticationStrategy in Bootstrap.grooby as described in my other blog post..

And that’s pretty much it!! As a very last step we register our concurrencyFilter in the spring security filter chain and then we’re done.

class BootStrap {

  def authenticationManager
  def concurrentSessionController
  def securityContextPersistenceFilter

  def init = { servletContext ->
    SpringSecurityUtils.clientRegisterFilter('concurrencyFilter', SecurityFilterPosition.CONCURRENT_SESSION_FILTER)
  }

  def destroy = {
  }
}

Comments are closed.