Wednesday, August 8, 2012

Using Spring Security Concurrent Session controls with Grails

While it isn't a problem for most website, many have a requirement to control how many users can be connected concurrently. This is usually related to sites that sell 'per user' licenses, such as my current company. Unfortunately, this isn't very straightforward in Grails.

First thing to note: Grails Spring Security plugin doesn't support concurrent session. Spring security does have the feature, and the plugin doesn't prevent it from being used, but it doesn't help either. And it's a bit harder because you don't have the advantage of the spring security namespace to help with some of this. Most of the information I found on how to set this up came from this blog post: http://blog.block-consult.com/2012/01/restricting-concurrent-user-sessions-in-grails-2-using-spring-security-core-plugin/. Using that as a guide, There are three things that have to be added to resources.groovy:
sessionRegistry(SessionRegistryImpl)

concurrencyFilter(ConcurrentSessionFilter) {
    sessionRegistry = sessionRegistry
    logoutHandlers = [ref("rememberMeServices"), ref("securityContextLogoutHandler")]
    expiredUrl='/login/invalidated'
}


concurrentSessionControlStrategy(ConcurrentSessionControlStrategy, sessionRegistry) {
    alwaysCreateSession = true
    exceptionIfMaximumExceeded = true
    maximumSessions = 1
}
The SessionRegistry is just a mechanism with a thread-safe map to register new sessions. It's stores them both by session id and by principal. It uses a class called SessionInformation. This object contains some information, such as the last time anything happened on this session, and if it's expired. In general, sessions are only registered by the ConcurrentSessionControlStrategy. This strategy actually extends the Spring SessionFixationStragey, which is a whole other ball of wax I won't go into. Either way, this strategy is generally used by the UsernamePasswordAuthenticationFilter. Normally I imagine this is setup using the spring namespace. However, in grails you have to deal with it manually. The way I found for it to work is an entry in boostrap.groovy:
authenticationProcessingFilter.sessionAuthenticationStrategy = concurrentSessionControlStrategy
What this strategy does is enforce *at authentication time* various rules about concurrent sessions. In our case, we set the Maximum sessions to 1. I also set it to throw an exception if it's exceeded. It basically stops the authentication. I do this because I would prefer to tell the user what will happen if they log in: the other session will be expired. This is done by the strategy by looping through the SessionInformation objects tied to the given principal and calling expireNow() on the least recently used one.

Which brings me to the filter.

The filter shown above is inserted into the standard spring security authentication filter chain. From the blog post above, it can be added to the filter chain using the following code in bootstrap.groovy:
SpringSecurityUtils.clientRegisterFilter('concurrencyFilter', SecurityFilterPosition.CONCURRENT_SESSION_FILTER)
Basically, what the filter does is call the SessionRegistry for the given session id. If there is a session information and it is marked as expired, it forces a redirect to the provided 'expiredUrl'.

Once this is done, it generally works as you expect. If you prevent a user from logging in, it's fairly simple to mark other session information as 'expired' and the filter will then redirect them to the 'expiredURL'.

Unless you're using Remember me, in which case everything is screwed.

If you notice in the config of the filter, there's a couple of log out handlers, and one of them is the remember me service. Which removes the remember me token when the filter 'expires' the session. But there's still a gigantic problem: RememberMe tokens are processed by a completely different filter that is oblivious to the concurrent session control. This is the RememberMeAuthenticationFilter. Keep in mind that the username and password filter is only called for a specific url and is usually posted to by some login page. So, it only touches a request if the url of the request matches the one it's looking for.

So, here's the scenario: User A logs in with a remember me token. The normal UsernamePasswordAuthenticationFilter is called. Because there's the remember me option enabled. (easily setup by the grails plugin) It tells the RememberMeServices to add the cookie to the response. Everything is fine after that. So, let's say that user sits idle for a bit and their session expires. The SessionRegistryImpl is also setup to receive HttpSession change events. This is setup by turning on the session event publisher via an option in config.groovy. Something like: grails.plugins.springsecurity.useHttpSessionEventPublisher = true. Which, is also necessary for concurrent session handling to work.

The container expiring the session causes the registry to remove the SessionInformation for this session from the registry. Now, let's say another user logs in with the same credentials (the same principal in Spring Security terms) That user is logged in and now has a session tied to that principal in the registry. Now, let's say that the first user with the remember me cookie starts using the application again. This creates a request that doesn't have a security context, which would normally punt you back to login. However, the RememberMeAuthenticationFilter sees the cookie and authenticates the user. In this case, it's actually a slightly different Authentication object, but the user is still authenticated. The RememberMe filter never looks at the registry or strategy, so as far as the SessionRegistry is concerned, there is still only one user for this principal. Thus the user with the token can use it all day long without running afoul of the concurrent session limits. What this basically means is that anyone using Spring Security with remember me can bypass concurrent session limits by using a remember me token and letting their session expire.

My first assumption was that there was something that needs to be setup that wasn't being done. Maybe something the namespace was doing. But I can't find any hook in either the remember me filter of the service for anything that would handle this. Even looking at newer versions of spring security, the only changes I see are for the registry to use java.util.concurrent, which was probably done after 3.1 when they could assume Java 5.

There are some pretty reasonable extension points to the filter though. I extended it and overrode the method onSuccessfulAuthentication, thinking I could do some of the same things the strategy does. I could register a new information and mark others as expired. But it gets a bit complicated. In this case, I think the users with the token coming back from idle should be the one bounced. Which isn't super hard, as long as the remember me filter is before the ConcurrentSessionFilter. However, there's all kinds of potential threading issues. Especially if an application uses ajax. It's really non-deterministic. I honestly think that the registry would need to be upgraded to understand the RememberMe cookie.

Usually, when working with an established open source project, I assume that I'm doing something wrong. However, when looking through the code, I can't find anything to handle this properly.

So, my current bottom line is this: Concurrent Session can be used with the Grails plugin, however, not if you allow your users to use a 'remember me' token.

Update

I had passed on this issue to some friends at Vmware, and it looks like I was correct: Concurrent Session doesn't work with RememberMe. Here's the ticket that was created: https://jira.springsource.org/browse/SEC-2028

2 comments:

BadBob said...
This comment has been removed by the author.
BadBob said...

Great and very detailed article. It helps me so much. Thanks a lot.
Your issue was closed as duplicate. Actual issue is: https://jira.springsource.org/browse/SEC-2028
Everyone, who read this article, please vote for that issue.