Session Timeout and Concurrent Session Control with Spring Security and Spring-MVC

by Quinten KrijgerFebruary 28, 2014
security-icon

A web application me and my team are building recently underwent a security review. As usual, because you haven’t yet had time to put any real effort into it, some security risks did surface. We use Spring Security and Spring-MVC and I will talk about implementing a session timeout and concurrent session control: nice subjects from the trenches.

In general, sessions should be managed as restrictively as possible for your web application. Category number two on OWASP top ten security threats of 2013 is broken authentication and session management. Here you can find some nice examples of the problem never lying with the internet, but with the human mistakes in using it.

Session timeout

An obvious mistake a user can make is forgetting to log out on a public computer, or if he’s unlucky – on a private device that is stolen at a later time. This alone is enough reason to invalidate a user session after a certain time, e.g. fifteen minutes. Another reason is that this also limits possibilities for smart hackers: especially in combination with other attacks like CSRF or click-jacking, session hijacking is a big risk. Moreover, if an account is important enough, physical break-ins to get to a device with a session that remains valid is not unimaginable. For these reasons it is also very important never to expose a session ID in a URL, but I’m getting off topic here …

Setting the timeout is easy, so this really is a quick win. In your web.xml, add the timeout to your session-config:

<session-config>
    <session-timeout>15</session-timeout>
    <tracking-mode>COOKIE</tracking-mode>
</session-config>

The tracking-mode plays no essential role here, but it is what we use in our app.

Concurrent session control

Concurrent session control for our use case amounts to controlling the number of sessions a user can have at the same time. Normally, each user has its own account, so it’s logical that a user can be logged in only once at a time. Since users might use different devices you might want to allow more than one session, but a maximum for the number of sessions is good practice.

Restricting the number of concurrent sessions will make sure accounts can no longer be shared between multiple or too many users. While not strictly security as I see it, this is also useful if you supply software with paid subscription accounts. For a security critical application, it is likely that if a user tries to log in a second time he or she forgot to log out the first time. In that case, it is a good idea to log out the user from the initial session, which is the approach we took.

We specified the timeout in the web.xml, which is handled by the servlet container (Tomcat in our case) – no Spring Security involved yet. However, to apply application logic to your sessions your application needs knowledge of the user sessions. You need to inject a session events listener in your web.xml:

<listener>
  <listener-class>org.springframework.security.web.session
    .HttpSessionEventPublisher</listener-class>
</listener>

The HttpSessionEventPublisher listens to the servlet container for sessions being created or destroyed and publishes corresponding Spring Security events. This will make sure that when a timeout triggers a session invalidation the application will know about it. We will manage session concurrency in the application, so this is critical. Also note that injecting this listener provides the SessionRegistry, which can now be @Autowired even if you have not defined it explicitly. The SessionRegistry is a nice entry point to get information on the current logged in users, even if you’re not interested in concurrent session control.

Next is injecting a ConcurrentSessionControlAuthenticationStrategy. Using the security xml namespace makes this real easy. In your security configuration, add

<beans:beans xmlns="http://www.springframework.org/schema/security" ... />
<http>
  <session-management>
    <concurrency-control
       max-sessions="1"
       expired-url="/your-page-here" />
  </session-management>
</http>

Note that the element automatically registers a ConcurrentSessionFilter in the filter stack. Set max-sessions="-1" in case you do not want to limit the number of sessions, but still would like a reference to the SessionRegistry as explained above.

Behaviour when Concurrent Sessions are exceeded

There are two possible ways to have your applications handle a login attempt that exceeds the allowed number. The default is that previous sessions are invalidated and if the user tries to use his initial session he will be redirected to “your-page-here”. This is probably best for most applications. The other option is to throw an exception after the login attempt and keep the former session(s) valid. To do that, add the attribute error-if-maximum-exceeded="true" to the <concurrency-control/>.

Warning: implement equals() and hashcode() on your UserDetails

Most applications implement their own user repository and UserDetails implementation. In that case, a warning is in order. Using some of the default Spring Security classes you get the following: ConcurrentSessionControlAuthenticationStrategy calls SessionRegistryImpl.getAllSessions() for the principal, which uses a Map from principal to sessions. The principal is a UserDetails instance, so if you implements UserDetails the equals() and hashcode() should be overridden. Otherwise, the strategy will not work.

Note on multi-node environments

The setup for concurrent session control up to now works perfectly for single node setups. However, if the web application is hosted on multiple servers you need to write your own implementation of SessionRegistry to have a single session registry for all nodes. However, the current implementation of ConcurrentSessionControlAuthenticationStrategy expires the session object directly (which is a simply bean built by the registry to represent the session), instead of delegating this to the registry. So, until this is changed in Spring Security, you would also have to implement and inject your own SessionAuthenticationStrategy.