Solving the logout problem properly and elegantly

Solutions for JSP pages and Struts

Many Web applications do not contain overly confidential and personal information like bank account numbers or credit card data. But some do contain sensitive data that requires some sort of password protection scheme. For example, in a factory where workers must use a Web application for entering timesheet information, accessing their training courses, and reviewing their hourly rates, etc., employing SSL (Secure Socket Layer) would be overkill (SSL pages are not cached; the discussion of SSL is beyond the scope of this article). But certainly these applications do require some kind of password protection. Otherwise, workers (in this case, the application's users) would discover sensitive and confidential information about all factory employees.

Similar examples to the situation above include Internet-equipped computers in public libraries, hospitals, and Internet cafes. In these kinds of environments where users share a few common computers, protecting users' personal data is critical. At the same time, well-designed and well-implemented applications assume nothing about the users and require the least amount of training.

Let's see how a perfect Web application would behave in a perfect world: A user points her browser to a URL. The Web application displays a login page asking the user to enter a valid credential. She types in the userid and password. Assuming the supplied credential is correct, after the authentication process, the Web application allows the user to freely access her authorized areas. When it's time to quit, the user presses the page's Logout button. The Web application displays a page asking the user to confirm that she indeed wants to log out. Once she presses the OK button, the session ends, and the Web application presents another login page. The user can now walk away from the computer without worrying about other users accessing her personal data. Another user sits down at the same computer. He presses the Back button; the Web application must not show any of the pages from the last user's session. In fact, the Web application must always keep the login page intact until the second user supplies a valid credential—only then he can visit his authorized area.

Through sample programs, this article shows you how to achieve such behavior in a Web application.

JSP samples

To efficiently illustrate the solution, this article starts by showing the problems encountered in the Web application, logoutSampleJSP1. This sample application represents a wide range of Web applications that do not handle the logout process properly. logoutSampleJSP1 consists of the following JSP (JavaServer Pages) pages: login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp. The JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp are protected against unauthenticated users, i.e., they contain secure information and should never appear on the browsers either before the user logs in or after the user logs out. The page login.jsp contains a form where users type in their username and password. The page logout.jsp contains a form that asks users to confirm that they want to indeed log out. The JSP pages loginAction.jsp and logoutAction.jsp act as the controllers and contain code that carries out the login and logout actions, respectively.

A second sample Web application, logoutSampleJSP2 shows how to remedy logoutSampleJSP1's problem. However, logoutSampleJSP2 remains problematic. The logout problem can still manifest itself under a special circumstance.

A third sample Web application, logoutSampleJSP3 improves upon logoutSampleJSP2 and represents an acceptable solution to the logout problem.

A final sample Web application logoutSampleStruts shows how Jakarta Struts can elegantly solve the logout problem.

Note: The samples accompanying this article have been written and tested for the latest Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox, and Avant browsers.

Login action

Brian Pontarelli's excellent article "J2EE Security: Container Versus Custom" discusses different J2EE authentication approaches. As it turns out, HTTP basic and form-based authentication approaches do not provide a mechanism for handling logout. The solution therefore is to employ a custom security implementation, as it provides the most flexibility.

A common practice in the custom authentication approach is to retrieve user credentials from a form submission and check against the backend security realms such as LDAP (lightweight directory access protocol) or RDBMS (relational database management system). If the supplied credential is valid, the login action saves some object in the HttpSession object. This object's presence in HttpSession indicates that the user has logged in to the Web application. For clarity's sake, all accompanying sample applications save only the username string in the HttpSession to denote that the user is logged in. Listing 1 shows a code snippet contained in the page loginAction.jsp to illustrate the login action:

Listing 1

//...
//initialize RequestDispatcher object; set forward to home page by default
RequestDispatcher rd = request.getRequestDispatcher("home.jsp");
//Prepare connection and statement
rs = stmt.executeQuery("select password from USER where userName = '" + userName + "'");
if (rs.next()) { //Query only returns 1 record in the result set; only 1 
  password per userName which is also the primary key
   if (rs.getString("password").equals(password)) { //If valid password
      session.setAttribute("User", userName); //Saves username string in the session object
   }
   else { //Password does not match, i.e., invalid user password
      request.setAttribute("Error", "Invalid password.");           
      rd = request.getRequestDispatcher("login.jsp");
   }
} //No record in the result set, i.e., invalid username
   else {
      request.setAttribute("Error", "Invalid user name.");
      rd = request.getRequestDispatcher("login.jsp");
   }
}
//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp"
rd.forward(request, response);
//...

In this and the rest of the accompanying sample Web applications, the security realm is assumed to be an RDBMS. However, this article's concept is transparent and applicable to any security realm.

Logout action

The logout action simply involves removing the username string and calling the invalidate() method on the user's HttpSession object. Listing 2 shows a code snippet contained in the page logoutAction.jsp to illustrate the logout action:

Listing 2

//...
session.removeAttribute("User");
session.invalidate();
//...

Prevent unauthenticated access to secured JSP pages

To recap, upon a successful validation of the credentials retrieved from the form submission, the login action simply places a username string in the HttpSession object. The logout action does the opposite. It removes the username string from HttpSession and calls the invalidate() method on the HttpSession object. For both the login and logout actions to be meaningful at all, all protected JSP pages must first check the username string contained in HttpSession to determine if the user is currently logged in. If HttpSession contains the username string—an indication that the user is logged in—the Web application would send to the browsers the dynamic content in the rest of the JSP page. Otherwise, the JSP page would forward the control flow back to the login page, login.jsp. The JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp all contain the code snippet shown in Listing 3:

Listing 3

//...
String userName = (String) session.getAttribute("User");
if (null == userName) {
   request.setAttribute("Error", "Session has ended.  Please login.");
   RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
   rd.forward(request, response);
}
//...
//Allow the rest of the dynamic content in this JSP to be served to the browser
//...

This code snippet retrieves the username string from HttpSession. If the username string retrieved is null, the Web application interrupts by forwarding the control flow back to the login page with the error message "Session has ended. Please log in.". Otherwise, the Web application allows a normal flow through the rest of the protected JSP page, thus allowing that JSP page's dynamic content to be served.

Running logoutSampleJSP1

Running logoutSampleJSP1 produces the following behavior:

  • The application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served if the user has not logged in. In other words, assuming the user has not logged in but points the browser to those JSP pages' URLs, the Web application forwards the control flow to the login page with the error message "Session has ended. Please log in.".
  • Likewise, the application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served after the user has already logged out. In other words, after the user has already logged out, if he points the browser to the URLs of those JSP pages, the Web application will forward the control flow to the login page with the error message "Session has ended. Please log in.".
  • The application does not behave correctly if, after the user has already logged out, he clicks on the Back button to navigate back to the previous pages. The protected JSP pages reappear on the browser even after the session has ended (with the user logging out). However, continual selection of any link on these pages brings the user to the login page with the error message "Session has ended. Please log in.".

Prevent the browsers from caching

The root of the problem is the Back button that exists on most modern browsers. When the Back button is clicked, the browser by default does not request a page from the Web server. Instead, the browser simply reloads the page from its cache. This problem is not limited to Java-based (JSP/servlets/Struts) Web applications; it is also common across all technologies and affects PHP-based (Hypertext Preprocessor), ASP-based, (Active Server Pages), and .Net Web applications.

After the user clicks on the Back button, no round trip back to the Web servers (generally speaking) or the application servers (in Java's case) takes place. The interaction occurs among the user, the browser, and the cache. So even with the presence of Listing 3's code in the protected JSP pages such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp, this code never gets the chance to execute when the Back button is clicked.

Depending on whom you ask, the caches that sit between the application servers and the browsers can either be a good thing or a bad thing. These caches do in fact offer a few advantages, but that's mostly for static HTML pages or pages that are graphic- or image-intensive. Web applications, on the other hand are more data-oriented. As data in a Web application is likely to change frequently, it is more important to display fresh data than save some response time by going to the cache and displaying stale or out-of-date information.

Fortunately, the HTTP "Expires" and "Cache-Control" headers offer the application servers a mechanism for controlling the browsers' and proxies' caches. The HTTP Expires header dictates to the proxies' caches when the page's "freshness" will expire. The HTTP Cache-Control header, which is new under the HTTP 1.1 Specification, contains attributes that instruct the browsers to prevent caching on any desired page in the Web application. When the Back button encounters such a page, the browser sends the HTTP request to the application server for a new copy of that page. The descriptions for necessary Cache-Control headers' directives follow:

  • no-cache: forces caches to obtain a new copy of the page from the origin server
  • no-store: directs caches not to store the page under any circumstance

For backward compatibility to HTTP 1.0, the Pragma:no-cache directive, which is equivalent to Cache-Control:no-cache in HTTP 1.1, can also be included in the header's response.

By leveraging the HTTP headers' cache directives, the second sample Web application, logoutSampleJSP2, that accompanies this article remedies logoutSampleJSP1. logoutSampleJSP2 differs from logoutSampleJSP1 in that Listing 4's code snippet is placed at the top of all protected JSP pages, such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp:

Listing 4

//...
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
String userName = (String) session.getAttribute("User");
if (null == userName) {
   request.setAttribute("Error", "Session has ended.  Please login.");
   RequestDispatcher rd = request.getRequestDispatcher("login.jsp");
   rd.forward(request, response);
}
//...

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.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies