Thursday, July 16, 2009

Logging out with HTTP Basic Authentication

This post arose from the fact that I have an application running on an application server that doesn't seem to support HTTP BASIC authentication very well (Glassfish v2, if you're interested). So I have a web-server sat in front of the application server, doing the authentication, and passing through all authenticated requests to the application server behind. (This configuration also works around a bug in Glassfish v2, where the HTTP response size is wrongly calculated on static resources, which confuses Seamonkey, although Firefox copes ok)

There is a downside to this arrangement, and that is that HTTP BASIC authentication doesn't really support the concept of logging out!

As normal with a Java web application, the logout operation is a matter of invalidating the current HttpSession, throwing away any cached user configuration, and redirecting the user to a 'you are logged out' page. This can be achieved by having a filter or a servlet pick up the 'logout' request, mark the HttpSession associated with the current HttpRequest object, and deleting the JSESSION cookie that identified the session. The response then just needs to contain a logout message, or redirect, or whatever you desire for your site.

That's all well and good if it's just the web application involved. However, in our scenario, we have web server to consider too. As it stands, there's nothing in the HyperText Transfer Protocol ('HTTP' to you and me) that allows the web application to tell the web server that the HttpSession is over - the application doesn't talk to the server, so (according to almost all the web sites I could find with Google) there's no way to tell the server to invalidate it's knowledge of the end user and their credentials.

Or is there....

The key factor is that it is possible to make the server re-challenge the user for their credentials in the same way as they were when they started using the application, and this can be made to have the same effect as logging them out. Note that the critical fact that previous web-pages on this subject seem to have missed is that HTTP BASIC authentication has a realm parameter.

By knowing what the realm is that the web-server used to the authenticate the user, we can cause the browser to re-authenticate against that same realm, by challenging the browser with an HTTP 401 (just like the web-server does).

The process (in my application) works like this:

  1. The user clicks the logout link to go to /logout.html, which tells the user they are being logged out. This is a nice-to-have page to make this process a little more friendly, you could skip it and go straight to the next step
  2. The browser pauses for 1 second on this page, then redirects to /logout
  3. This url is mapped to a logout filter, which does the normal session termination activities I mentioned above. But in order for the next step to work, the filter also registers the current HttpSession key with the LoggedOutServlet (storing it in a singleton HashSet of keys), and creates a has-logged-out cookie with the HttpSession key as the value of the cookie.
  4. The logoutFilter then redirects to /loggedout which has been mapped to the LoggedOutServlet in my web.xml:
  5. <servlet>
      <description>Logout Servlet</description>
      <display-name>LoggedOutServlet</display-name>
      <servlet-name>loggedout</servlet-name>
      <servlet-class>
        com.xyzzy.web.LoggedOutServlet
      </servlet-class>
    </servlet>
    
    <servlet-mapping>
      <servlet-name>loggedout</servlet-name>
      <url-pattern>/loggedout</url-pattern>
    </servlet-mapping>
    

  6. The LoggedOutServlet looks for the has-logged-out cookie. If cookie exists, and the value is in the servlet's register of logging-out keys, then the key is removed from the register, the cookie marked as deleted, and an Unauthorised (HTTP 401) response is returned to the user:
    public void setupResponseForLogout(HttpServletResponse response) {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // HTTP 401
        response.setHeader("WWW-Authenticate", "Basic realm=\"xyzzy\"");
    }
    

  7. The user is then prompted by the browser to re-authenticate. The critical thing here is that the realm (set to 'xyzzy' above) is set correctly in the response. If this is done, then the web-server (which must be authenticating with the same realm) will correctly re-authenticate the user if they try to login again. So the consequence is that the user is effectively logged out, and cannot get back into the application without being challenged for their credentials again. This works because the browser has been asked to authenticate against a specific realm, so that will be used in the authentication process, and will force the web server to full re-check the user's credentials - refusing access if they get it wrong.
NB. all the urls used in this process must be accessible without logging in (i.e. anonymously).

1 comment:

  1. hi ben,

    thanks for this nice explaination it was helpful to me

    ReplyDelete