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);
}
//...
1 2 Page 1
Page 1 of 2
How to choose a low-code development platform