Clearing Cookies in Spring & Tomcat


I haven’t been blogging very much (besides my most recent post, of course) and I mean to change that. The reasons I stopped are varied and multifarious, but one of the most prominent ones is that I scored a full-time internship for the summer at Joule Assets, doing web programming. Recently (i.e. today) I encountered a particularly frustrating bug, and I figured I’d contribute to the common good and write about it.

We’ve been working on a webapp utilizing Spring, a web framework for Java. We have this running on a Tomcat server, and are developing through Eclipse. A particularly frustrating mix of technologies.

Ok, well, enough of bashing on Eclipse - that’s a topic for another post. We were talking about the webapp.

Recently, my boss tasked me with solving a particular problem, going something like this:

  1. Person logs into our webapp.

  2. Person does not log out of our webapp. The server waits patiently for thirty minutes, or however long the session timeout it set to.

  3. Person tries to access a webpage they previously had access to, and they get booted to the login screen with a message telling them their session expired.

Ok, well, that’s not the problem. Session timeouts are actually a good idea for many reasons. The real problem is this:

  1. Person logs into our webapp.

  2. Person logs out of our webapp.

  3. They _still _get booted to the login screen with the session timeout error message.

Why does this happen? After analyzing the request/response headers and what gets sent around, we got a better picture. What’s actually going behind the scenes is a little more complex:

  1. Person logs in with their credentials.

  2. Server sees that the credentials are correct, and issues the person a session ID in the form of a cookie.

  3. Person stores the cookie in their browser, does some stuff in the webapp for a while.

  4. Person hits the logout button.

  5. Server gets the logout request, and dutifully invalidates the person’s session. However, the person retains the old cookie.

  6. Server tells the person he should probably go to the login page now, since he’s just logged out.

  7. Person sees that the server wants to redirect them to the login page. He looks in his cookie jar and sees - oh! - a cookie belonging to the server! He gives it to the server, and asks the server for the login page, since the server told him he should probably go there.

  8. Server sees that this cookie belongs to an invalid session. It shouts at the person a bit for him to go to the error page.

  9. Person goes to the error page, sufficiently chastised.

So. What we really want to do is invalidate the cookie so that we can tell the browser not to use it anymore, and then we can differentiate between a session timeout and a successful login. Of course, this isn’t such an easy thing to do, because the server can’t directly control what goes on on the client. If it could, bad things would happen. Imagine a world where you go to some website and it could install whatever program it wanted on your computer. Oh, wait.

Well, anyway, the solution to this is to set the expiry date of the cookie to right now. In cookie jargon, this means setting the maximum age of the cookie to 0. This will tell the browser that they probably shouldn’t be using the cookie anymore, and it seems like most of the time, they’ll obey this directive; it seems to be a fairly good way to do this. Except in IE, of course.

Ok, well, let’s do that. Well, it seems to be working, at least on Firefox on Windows. Unfortunately, it’s broken on Chrome on Windows. But wait… it’s broken on Firefox on Linux? And it’s working on Chrome on Linux? And, get this… it’s working in IE!? How is this possible?

We potter around the internet for about a half an hour, looking for some way to express this problem in a way Google will understand. Unfortunately, all the misinformation/misdeeds going on around cookies obscure any helpful information we might find. Eventually, we just try to debug it ourselves. We figure out how to look at cookies manually on Firefox. It’s hidden away in the Tools->Options->Privacy menu, within a link saying “remove individual cookies”.

Ok, so we compare the cookies across Firefox and Linux and Windows. They’re both the same… except there’s a slash missing from the cookie’s path on Linux? What the hell does the path even do? That can’t possibly be the issue…

Except we try clearing both of them in logout handler, and it works just fine.

What The FFFFFFFFFFFFF -

Honestly, the real puzzling issue was not really that we needed to add on that slash. The real puzzler is that it not only worked differently on different browsers on the same platform, it worked differently on the same browser on different platforms! Honestly, I don’t care too much to look into why, but if you’ve got an idea, let me know in the comments.

For those who are interested, here’s the code. You’ve got to implement your own LogoutSuccessHandler, which isn’t too hard. Here’s how we did it: we made a class that overrides SimpleUrlLogoutSuccessHandler. Then, we defined our LogoutSuccessHandler as a bean in our security-context.xml, and used the success-handler-ref attribute in the security:logout definition in the XML. Finally, we overrode onLogoutSuccess() in the custom LogoutSuccessHandler like so:

void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
    //This search may or may not actually be necessary, you might be able to just
    // create a new cookie with the right name
    for(Cookie cookie : request.getCookies()) {
        if(cookie.getName() == "JSESSIONID") {
            //Clear one cookie
            cookie.setName("");
            cookie.setMaxAge(0);
            cookie.setPath(request.getContextPath());
            response.addCookie(cookie);
            //Clear the other cookie
            Cookie cookieWithSlash = cookie.clone();
            cookieWithSlash.setPath(request.getContextPath() + "/");
            response.addCookie(cookieWithSlash);
        }
    }
    //This is actually a filter; continue along the chain
    super.onLogoutSuccess(request, response, authentication);
}

Oh, and before you say it:

Yes, thank you, I know.

(Thanks to TheProfoundProgrammer.com for the great posters)