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).

Wednesday, July 01, 2009

TreeSet and implementing the Comparator interface

Time for a bit more technical content. You might begin to discern a pattern here: gotchas in equals(), hashcode() and Java Collections.

My experience of Collections thus far has taught me that most of them are built upon detecting object equality through the equals() and hashcode() implementation in your class. See my earlier post and the posts of Andy Beacock referenced therein. However, today I ran into problems with objects vanishing in a TreeSet.

Back to the requirements first. I have list of Bank Account objects I wish to show on a web-form, but that list should also be modifiable by the user with an Add Form, and a Remove button on each listed account. So far so good. One more thing, the list has got to be in account name order on display. For this example, a Bank Account consists of a pseudo-primary key (id), a Bank Name, an Account Name, an Account Number, and a Sort Code. The id is not shown, and the records are unique by Account Number and Sort Code (and therefore implicitly by Bank Name).

Obviously, Bank Names should should not really be store in a Bank Account record - they should be stored separately and referenced by foreign key, as indeed, should Sort Codes - in fact, in an ideal world, the Bank Account record would be an id, a name, an account number and a foreign key to a branch as identified by the sort code. However, you know as well as I do that this is not an ideal world, and most of the time, we have to deal with the world as we come to it.

So, back to my slightly contrived example. I'm going to ignore completely all the contextual information about display technology, form interaction, transactions, etc., and concentrate on the core issue: SortedSets are nasty wee buggers!

There are two ways to manage the display of sorted data: sort on insert, and sort on display. If you're displaying often, and inserting & deleting infrequently (as in this example), sort on insert is preferable, otherwise you spend a lot of time sorting your data again and again with no change to the data.

Having looked at the implementations of the SortedSet interface, we really only have one available in the Java SDK (I'm staying away from the Apache Collections for now), and that is TreeSet, for which you specify a Comparator at construction time. Naively, I assumed that my comparator would just be interested in the Account Name - that's the field by which I wish to sort, after all.

    @Override
    public int compare(BankAccount o1, BankAccount o2) {
        return o1.getAccountName().compareTo(o2.getAccountName());
    }


(I'm leaving out the normal defensive programming and exception handling for clarity)

However, my BankAccount class implements equals() and hashcode() as normal, with the sortcode and the account number - these being the 'business key' if you like. So, on this basis, I expected TreeSet to sort on name, and use equals() and/or hashcode() to determine whether the Set contains a given BankAccount on insertion - in the same way that HashSet would.

Ooops, no.

Turns out that TreeSet doesn't use equals() or hashcode() at all. It uses the Comparator for sorting and contains() checking. So now, my Comparator implementation looks like this:

@Override
public int compare(BankAccount o1, BankAccount o2) {
   int result = o1.getAccountName().compareTo(o2.getAccountName());
   if (result == 0) {
       result = o1.getSortCode().compareTo(o2.getSortCode());
   }
   if (result == 0) {
       result = o1.getAccountNumber().compareTo(o2.getAccountNumber());
   }
   return result;
}

Now I have data sorted on insert and display, and the data doesn't get lost if someone enters two accounts with the same Account Name.

In conclusion, I think that the only reason this gotcha got me was that I was motivated to use the TreeSet simply by the requirement to sort by name, so I wrote the Comparator with only that in mind. A correct implementation of really should include the business keys of the entity under consideration... of course, I'd just done that, I might have had even more trouble with the sort-by-name requirement (i.e. a non-key property).