Wednesday, December 19, 2012

Varnish Cache and remember-me cookies

Automatically authenticating a previously logged in user upon their next visit (a feature often called "remember me") is an established Internet norm. While the exact implementations vary, they always rely on a special cookie that is set on the client and later recognized as a token signifying the client should be logged in automatically.

The problem

The cookie solution is perfectly fine, but in some environments, where cookies carry additional significance, it might be a source of problems. One such example is when the server is sitting behind a caching solution, such as Varnish. By default, Varnish will let all cookie-carrying requests pass right through it, without doing any processing or caching. This is a very sensible default behavior as it ensures that no personalized content will end up cached and returned to the wrong user, potentially revealing sensitive information. On the other hand, it also means that all clients sending any cookies (remember-me cookies included) will not benefit from the acceleration provided by the cache, nor will the server benefit from the decreased load in these cases as it will get hit every time. This will, of course, occur even if the cookies carry no information used for personalization of the content (as is the case with the remember-me cookie). Luckily, Varnish permits a very fine-grained handling of requests and responses through its excellent domain-specific configuration language, VCL. Yet, simply configuring Varnish to normally process requests carrying (only) the remember-me cookie is not a solution. The initial request carrying the remember-me cookie must pass through the cache and reach the application, so that the authentication process can take place, but all subsequent requests within the same session can be safely intercepted by Varnish (as long as they do not carry some other cookies with personalization info).

The problem that arises is how to distinguish the initial from all subsequents requests within the same session. Also, it must be taken into account that it is not predictable which URL within the application a cookie-carrying user will visit first. The user might visit the home page first, but might also land on some bookmarked page, or one they were sent to by a search engine. Checking the request for the presence of a session cookie might be enough, depending on whether this cookie is a temporary one or not. If it is temporary (and thus lost when the browser closes), this solution is probably enough. If the session cookie is persistent, it might get sent even after it had already expired, so its presence is not enough to determine whether the user has an active session.

A solution

One easy way to side-step all this complexity is to introduce an additional surely-temporary cookie which can be used as a flag indicating that the user has already signed in and that the request can be safely intercepted. Since this is a Java blog, the proposed solution will be demonstrated utilizing Spring Security, the platform's de facto standard security framework, but the general principle is reusable in any technology. Spring Security allows custom logic to fire after a successful log-in by implementing and registering an AuthenticationSuccessHandler. This is exactly the place where the new cookie should be added to the response.

Example

As with everything else in Spring, there are multiple ways to register a custom handler, but the most usual way is to add authentication-success-handler-ref attribute to the form-login element in your security context configuration XML.

<form-login ... authentication-success-handler-ref="authenticationSuccessHandler" ... />

<beans:bean id="authenticationSuccessHandler" class="com.example.security.AuthenticationSuccessHandler"/>

The class implementing the behavior should look similar to the following:

public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
     @Override
     public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        Cookie varnishCookie = new Cookie("VARNISH", "true");
        varnishCookie.setPath("/");
        varnishCookie.setMaxAge(-1); //It is very important that this cookie is temporary
        response.addCookie(varnishCookie);
     }
}

On Varnish side, it is enough to allow the request to pass if the remember-me cookie is present and the flag-cookie absent (signifying that the current request is the initial one):

if (req.http.cookie !~ "VARNISH" && req.http.cookie ~ "REMEMBER_ME") {
  return(pass);
}

This way, requests that should auto log-in the user will succeed, while subsequent ones will be intercepted normally, benefiting from Varnish.

Notes

  • Since there is bound to be some part of the page that will be specific to the logged-in user, include that part using ESI, and exclude that URL from Varnish processing. Varnish supports ESI:include so the setup is relatively simple.