Solving the logout problem properly and elegantly

Solutions for JSP pages and Struts

1 2 3 Page 2
Page 2 of 3

The combination of setting the headers directives and checking for the username in the HttpSession objects ensures that the browsers will not cache the JSP page. Also, if the user is not logged in, the JSP page's dynamic content will not be sent to the browsers, but instead the login.jsp page will.

Running logoutSampleJSP2

The following behavior results from running the sample Web application logoutSampleJSP2:

  • After the user logs out, attempts to click on the Back button will not cause the logout.jsp, secure1.jsp, and secure2.jsp JSP pages to reappear on the browser. Instead, the login page will appear on the browser, with the message stating "Session has ended. Please log in.".
  • However, whenever the Back button returns to a page that was the action target of a POST request (i.e., loginAction.jsp), the IE and Avant browsers will display a page with the following message:

    Warning: Page has Expired

    The page you requested was created using information you submitted in a form. This page is no longer available. As a security precaution, Internet Explorer does not automatically resubmit your information for you.

    To resubmit your information and view this Webpage, click the Refresh button.

    Mozilla and FireFox browsers on the other hand display a dialog box with the following message:

    The page you are trying to view contains POSTDATA that has expired from cache. If you resend the data, any action from the form carried out (such as a search or online purchase) will be repeated. To resend the data, click OK. Otherwise, click Cancel.

Selecting the Refresh command from the IE and Avant browsers or resending the data from the Mozilla and FireFox browsers will result in the previous JSP page reappearing on the browsers. Obviously, this is not desirable as it defeats the purpose of the logout action. When this happens, the implication is a malicious user can still access someone else's data. This problem, however, only manifests itself when the browsers, as a result of the Back button, return to a page that was the action target of a POST request.

Track the last logon time

The above problem occurs because the browsers resubmit the data from their cache. In this case, the data consists of the username and password. Despite displaying a security warning message, as in the case of IE, the browsers in fact create an opposite effect.

To fix this problem experienced in logoutSampleJSP2, logoutSampleJSP3's login.jsp should contain—in addition to the username and password—a hidden field called lastLogon that is dynamically initialized with a long value. This long value is obtained by calling System.currentTimeMillis() and denotes the current number of milliseconds since January 1, 1970. When the form in login.jsp is submitted, loginAction.jsp first compares the value from this field against the lastLogon field's value in the User database table. Only when the lastLogon value from the form is greater than the value in the database will it be considered a valid login.

For valid login, the field lastLogon in the database should be updated with the form's value to update the lastLogon's time. When the browsers resubmit from the cache as in the case above, the form's lastLogon value is not greater than the database's lastLogon value, therefore loginAction.jsp forwards the control flow back to login.jsp with an error message stating "Session has ended. Please log in.". Listing 5 shows the code snippet from loginAction.jsp:

Listing 5

//...
RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default
//...
if (rs.getString("password").equals(password)) { //If valid password
   long lastLogonDB = rs.getLong("lastLogon");
   if (lastLogonForm > lastLogonDB) {
      session.setAttribute("User", userName); //Saves username string in the session object
      stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'");
   }
   else {
      request.setAttribute("Error", "Session has ended.  Please login.");
      rd = request.getRequestDispatcher("login.jsp");        }
}
else   { //Password does not match, i.e., invalid user password
   request.setAttribute("Error", "Invalid password.");
   rd = request.getRequestDispatcher("login.jsp");   
}
//...
rd.forward(request, response);
//...

To facilitate the above approach, you must track each user's lastLogon time. For the RDBMS security realm, this can be easily accomplished by extending the schema of the User or an equivalent table by adding a column denoting the lastLogon time. LDAP or other security realms require more thought, but the lastLogon approach can certainly work with those realms.

There are probably many ways to represent the lastLogon time. The sample logoutSampleJSP3 utilizes the number of milliseconds since January 1, 1970. This method would also work in an extreme case where the same user account is used by multiple people from multiple separate browsers for logging in.

Running logoutSampleJSP3

Running the sample logoutSampleJSP3 shows that it handles the logout problem properly. Once the user has logged out, clicking on the browser's Back button will not cause any of the protected JSP pages to display under any circumstances. This sample Web application shows how the logout process is correctly handled without requiring any additional training or assumption on the users. No user experience is compromised.

In production-quality code, some redundant code can be eliminated. One way is to factor out the code in Listing 4 into a separate JSP page, which, in turn, can be included in other JSP pages by using the tag <jsp:include>.

Logout framework for Struts

Another alternative and a better approach to developing Web applications rather than straight JSP pages or JSP pages/servlets is to use Struts. For Struts-based Web applications, adding a framework to handle the logout problem properly can be easily and elegantly achieved. This is partially due to the fact that Struts is based on the Model-View-Controller design pattern and therefore allows a clear separation between the code in the model and in the view. In addition, as Java is an object-oriented language and supports inheritance, code reuse can be better achieved than any kind of scripting in JSP. With Struts, the code in Listing 4 can be moved out of the JSP page and into Action class's execute() method.

Furthermore, using inheritance, a base Action class can be defined to extend the Struts Action class and contain the code similar to Listing 4 in its execute() method. Other action classes can subclass the base Action class and, by virtue of inheritance, inherit the common logic for setting the header directives and checking for the username string in the HttpSession object. The base Action class is made abstract and defines an abstract method executeAction(). All subclasses of the base Action class must now implement the executeAction() method rather than override the execute() method. With this inheritance hierarchy in place, all of the base Action's subclasses no longer need to worry about any plumbing logout code. They would instead only contain code pertaining to their normal business logic. Listing 6 shows a portion of the base Action class:

Listing 6

  public abstract class BaseAction extends Action {
   public ActionForward execute(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response) 
      throws IOException, ServletException {
      
      response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server
      response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance
      response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale"
      response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility 
      
      if (!this.userIsLoggedIn(request)) {
         ActionErrors errors = new ActionErrors();
         errors.add("error", new ActionError("logon.sessionEnded"));
         this.saveErrors(request, errors);
         return mapping.findForward("sessionEnded");
      }
      return executeAction(mapping, form, request, response);
   }
   protected abstract ActionForward executeAction(ActionMapping mapping,
      ActionForm form, HttpServletRequest request, HttpServletResponse response) 
      throws IOException, ServletException;      
   private boolean userIsLoggedIn(HttpServletRequest request) {
      if (request.getSession().getAttribute("User") == null) {
         return false;
      }
      return true;
   }
}

Listing 6 resembles Listing 4, with the only difference being that ActionMapping findForward replaces RequestDispatcher forward. In Listing 6, if the username string is not found in HttpSession, the ActionMapping object will find the forward element named sessionEnded and forward the control flow to its corresponding path. Otherwise, the subclasses will be given a chance to carry their own business operations through their implementation of the executeAction() method. For this reason, it is important that the action forwards specified in the struts-web.xml configuration file for all the base Action's subclasses to declare a forward element named sessionEnded, with its path pointing to /login.jsp. Listing 7 uses the secure1 action to illustrate such a declaration:

Listing 7

<action path="/secure1" 
   type="com.kevinhle.logoutSampleStruts.Secure1Action"           
   scope="request">
   <forward name="success" path="/WEB-INF/jsps/secure1.jsp"/>
   <forward name="sessionEnded" path="/login.jsp"/>
</action>

The Secure1Action class subclasses from the BaseAction class, therefore providing implementation for the executeAction() method instead of overriding the execute() method. Secure1Action does not need to perform any logout plumbing code, as shown in Listing 8:

Listing 8

public class Secure1Action extends BaseAction {
   public ActionForward executeAction(ActionMapping mapping, ActionForm form,
      HttpServletRequest request, HttpServletResponse response)
      throws IOException, ServletException {
      
      HttpSession session = request.getSession();            
      return (mapping.findForward("success"));
   }
}

The above solution is effective and elegant since it does not require any convoluted work-around code—it only requires defining a BaseAction class. Factoring out common behavior in some sort of base Action class (that extends the Struts Action class) is recommended anyway and a common practice already for many Struts development projects.

Limitations

As simple and as elegant as the solutions presented above are for both JSP- and Struts-based Web applications, they do have a couple of limitations, one of which has a work-around. In any case, in my opinion, these limitations are rather minor:

  • By breaking the cache mechanism associated with the browsers' Back button as explained, data-entry-intensive form pages would lose all data entered to a page once the user leaves that page before submitting. Returning to that page with the browser's Back button won't help since a fresh blank page would now be fetched from the application server. A possible work-around is not to protect those JSP pages that contain data-entry forms. In the JSP-based solution, those JSP pages can omit the code in Listing 4. In the Struts-based solution, the Action class needs to subclass from the Struts Action class instead of the BaseAction class.
  • The approach presented does not work on Opera browsers. In fact, no known solution works on Opera browsers because Opera browsers follow Request For Comments 2616 Hypertext Transfer Protocol—HTTP/1.1 too closely. Section 13.13 of RFC 2616 states:

    User agents often have history mechanisms, such as "Back" buttons and history lists, which can be used to redisplay an entity retrieved earlier in a session.

    History mechanisms and caches are different. In particular history mechanisms SHOULD NOT try to show a semantically transparent view of the current state of a resource. Rather, a history mechanism is meant to show exactly what the user saw at the time when the resource was retrieved.

    Fortunately, Microsoft Internet Explorer and Mozilla-based browsers outnumber Opera browsers. The solution presented still satisfies a large number of users. In addition, with or without the solution presented, Opera browsers have always had the logout problem. Nothing has changed as far as Opera browsers are concerned. However, as specified in RFC 2616, with the headers' directives above, Opera browsers disallow stale pages to be served from the cache when a link is clicked.

Conclusion

This article has described the solutions to the logout problem. The solutions are surprisingly simple yet functional in all conditions. Whether for JSP or for Struts, all it takes is no more than 50 lines of code and a way to track the user's last logon time. Incorporating these solutions in a password-protected Web application benefits users by ensuring their private data cannot be compromised under any condition while improving the user experience.

Kevin H. Le has more than 12 years of experience in software development. In the first half of his career, his programming language of choice was C++. In 1997, he shifted his focus to Java. He has engaged in and successfully completed several J2EE and EAI projects as both developer and architect. In addition to J2EE, his current interests now include Web services and SOA. More information on Kevin can be found on his Website http://kevinhle.com.
1 2 3 Page 2
Page 2 of 3