Revisiting the logout problem

Recipes for JSF and Stripes

In my previous article, "Solving the Logout Problem Properly and Elegantly" (JavaWorld, September 2004), I addressed the logout functionality in a Web application, specifically the problem created by a browser's Back button. The solution I proposed indeed prevents users from accessing any restricted page via the Back button once they complete the logout process. However, if logged out users continue clicking the Back button, they will eventually come back to the resource that was the action of the login-form POST request. And at this point, the browser begins displaying confusing warning messages.

The confusing warning messages appear in IE browsers as follows:

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.

And in Firefox browsers:

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.

Now, without resupplying the user ID and password, by just simply clicking on the OK button, users (or hackers) can access restricted resources again, which defeats the purpose of the logout process. In my previous article, we tracked the last login time to address this security hole. This solution gets the job done, but is not ultimately perfect.

This article revisits the problems created by the browser's Back button and eliminates the remaining flaw by preventing such warning messages from ever appearing. The previous solution targeted JSP (JavaServer Pages) and Struts. In this article, I present a solution for both JavaServer Faces and Stripes, two popular Java-based Web frameworks. By the end of this article, readers will have two recipe-style JSF and Stripes solutions that many Web applications, such as Microsoft Office Project Web Access, still lack.

Note: I assume readers are already familiar with both JSF and Stripes, as this article is not a beginner's tutorial on how to use those frameworks.

The problem with JSP/servlet forward

The reason why the browser throws the confusing warning messages and allows a security hole must first be explained. The controller that is the action of a login-form POST "naively" dispatches a forward to a second JSP page upon authenticating the credential. Figure 1 depicts the control flow.

Figure 1. Forward dispatch after login form POS

The problem stems from the fact that a JSP/servlet forward dispatch is entirely internal to the container, and the browser is only aware of the original action resource, namely loginAction.jsp. After the login form submission (with a valid user ID/password) and the forward dispatch, the dynamic HTML content of securePage.jsp is served to the browser. With each subsequent browser's reload/refresh, the browser reloads loginAction.jsp, not securePage.jsp, since it only knows the former and the latter is completely transparent to it. Since loginAction.jsp is the action of a POST request that contains input data, the browser displays the warning messages for confirmation purposes. The browsers behave similarly when reloading the resource loginAction.jsp after users click the Back button. With the browsers remembering the POST input data, and with users (or malicious hackers) simply clicking the warning message's OK button, users (or hackers) can access restricted resources again, which defeats the purpose of the logout process. I came up with a solution that tracks last login time, which I described in my previous article, but it needs improvement.

Another problem, although minor, that has not been discussed is that while the browser renders the HTML code from the view securePage.jsp, the browser address bar shows loginAction.jsp. A majority of users will probably never pay attention to this detail and, even if they do, they probably won't care, but to the meticulous ones, something just appears out of sync.

The solution

The solution is surprisingly simple, as Figure 2 illustrates.

Figure 2. Redirect dispatch after login form POST

The controller loginAction.jsp finishes the authentication logic and performs a redirect dispatch instead of a forward dispatch to securePage.jsp. Essentially, the redirect involves an extra round-trip, where the Web application instructs the browsers to fetch the page securePage.jsp. The browser is then aware of both the original resource (login.jsp) and the second resource (securePage.jsp). As a result, the browser's history stack only contains login.jsp followed by securePage.jsp, while loginAction.jsp is transparent to the browser. This is exactly the preferred situation.

Both reloading/refreshing and the clicking of the Back button will never lead the browser to load loginAction.jsp, which is associated with a POST request. Since no POST request is repeated, there are no issues with POST input data, and the browser will not throw warning messages to users. A by-product of this solution is that it can be applied to solve problems due to double submission (a subject that reaches beyond this article's scope).

JSF recipe

This section presents the JSF recipe for our proposed solution in which the key is to perform a redirect right after the login form POST. Figure 3 depicts a page flow in a JSF application that handles login and logout functionality. Rectangle boxes represent JSF managed beans, and rounded rectangle boxes represent JSP pages. The caption in rectangle boxes is in the format of class.method, and the caption in rounded boxes denotes, not surprisingly, the JSP file name.

Figure 3. Page flow in the JSF recipe

The steps in the JSF recipe are now explained:

  1. The entry point is the index.jsp page, which is configured in web.xml as the welcome page. The place-holder index.jsp simply performs a forward to login.jsp.
  2. The login.jsp contains a JSF form with a couple of <h:inputText> tags (for user ID and password fields) and a <h:commandButton> tag (login button). Listing 1 shows an excerpt of login.jsp. Everything in this file is normal and standard JSF development, with one exception: the only item pertaining to the logout solution is the include of the file noCache.jsp. This include of noCache.jsp, shown in Listing 2, ensures that as the browser reloads the login.jsp page when the Back button is clicked (after logout occurs), the user ID's input text field appears empty and does not contain what users typed in earlier. This keeps the user ID unknown to hackers if they happen to gain physical access to the computer and click the Back button.

    Listing 1. login.jsp

      ...
    <%@ include file="noCache.jsp" %>
    ...
    
    <!-- Standard JSF from here down -->
    <html>
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=Cp1252"/>
          <title>jsf-ProperLogout</title>
       </head>
       <body>
          <f:view>
             <h:form>
                User Id ("guest"): <h:inputText value="#{service.userId}"/>
            <br/>
                Password ("abc123") : <h:inputSecret value="#{service.password}"/>
                <br/>
                <br/>            
                <h:commandButton value="Login" action="#{service.authenticate}" onclick="return"/>         
             </h:form>
          </f:view>
       </body>
    </html>
    

    Listing 2. noCache.jsp

      <%
    //Forces caches to obtain a new copy of the page from the origin server
    response.setHeader("Cache-Control","no-cache");
    //Directs caches not to store the page under any circumstance 
    response.setHeader("Cache-Control","no-store");
    //Causes the proxy cache to see the page as "stale" 
    response.setDateHeader("Expires", 0); 
    //HTTP 1.0 backward compatibility
    response.setHeader("Pragma","no-cache");    
    %>
    
  3. SecurityService is a managed bean and is responsible for authenticating the credential. SecurityService has a method called authenticate(). Depending on whether the credential is valid or not, authenticate() returns the string loginSuccess or loginFailure, respectively. In the case of loginSuccess, authenticate() saves the user ID in the session. Depending on the application requirements, one can choose to implement authenticate() by creating a user bean and then saving the user bean in the session. Since SecurityService is just a POJO (plain-old Java object), the session is not readily available. So to retrieve the session, a utility-type class called SessionUtil is created. Listing 3 shows the implementation of SecurityService.authenticate() in its entirety. Listing 4 shows the class SessionUtil.

    Listing 3. SecurityService.authenticate()

      ...
    import javax.servlet.http.HttpSession;
    import org.pragmaticobjects.jsfProperLogout.util.SessionUtil;
    
    public class SecurityService {
       private String userId;
       private String password;
          
       //Setters, getters for userId, password
       ...
       
       public String authenticate() {
          String action = "loginFailure";
          
          //Replace with more appropriate logic than what's shown here
          if (userId.equalsIgnoreCase("guest") && password.equalsIgnoreCase("abc123") ) {
               HttpSession session = SessionUtil.getSession();
               session.setAttribute("User", userId);
                action = "loginSuccess";            
            }        
                    
            return action;
       }
    
    }
    

    Listing 4. SessionUtil

      ...
    import javax.faces.context.*;
    import javax.servlet.http.*;
    
    public class SessionUtil {
       public static HttpSession getSession() {
          ExternalContext extCon = FacesContext.getCurrentInstance().getExternalContext();
          HttpSession session = (HttpSession) extCon.getSession(true);
          return session;
       }
    }
    
  4. Since SecurityService is a managed bean, it needs to be configured in the config file faces-config.xml. Listing 5 shows the portion of faces-config.xml where SecurityService is configured.

    Listing 5. SecurityService configuration in faces-config.xml

      ...
    <managed-bean>
       <managed-bean-name>service</managed-bean-name>
       <managed-bean-class>org.pragmaticobjects.jsfProperLogout.service.SecurityService</managed-bean-class>
       <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>
    ...
    
  5. From the page flow diagram shown in Figure 3, SecurityService redirects to securePage.jsp or loginError.jsp, depending on the case of loginSuccess or loginError, respectively. As we saw, the authenticate() method of SecurityService only returns a string, but does not explicitly perform any redirect. In a JSF application, all the plumbing has already been done. A redirect dispatch can be performed declaratively by configuring the navigation rule in the config file faces-config.xml. Listing 6, an excerpt of faces-config.xml, shows the navigation rule for the login process.

    Listing 6. Navigation rule configuration for login in faces-config.xml

      ...
    <navigation-rule>
       <from-view-id>/restrictedPages/login.jsp</from-view-id>
       <navigation-case>
          <from-outcome>loginFailure</from-outcome>
          <to-view-id>/restrictedPages/loginError.jsp</to-view-id>
          <redirect/>
       </navigation-case>
       <navigation-case>
          <from-outcome>loginSuccess</from-outcome>
          <to-view-id>/restrictedPages/securePage.jsp</to-view-id>
          <redirect/>
       </navigation-case>
    </navigation-rule>
    ...
    
  6. The JSP file loginError.jsp simply displays a login error message and provides a link back to login.jsp.
  7. In the case of loginSuccess, users gain access to the restricted resource securePage.jsp. This recipe only has one restricted resource, but in other applications, most likely, there will be many restricted JSP resources. The point is everything in securePage.jsp is standard JSF implementation, with only one item that pertains to the logout solution.

    Listing 7 shows an excerpt of securePage.jsp. The inclusion of noCache.jsp prevents the browser from caching securePage.jsp and ensures the browser always fetches a fresh copy when the Back button is clicked. Listing 7 also shows that the application checks the session for a user ID (or user bean, as mentioned in Step 3). If null, the application performs a redirect to login.jsp. Such action is necessary to ensure the browser only fetches the HTML content of securePage.jsp during the time between login and logout—in other words, the time after login and before logout. After the logout step, where the session is invalidated (as we shall see in Step 8), if the browser is requested to fetch securePage.jsp as a result of the Back button click, the browser will be instructed to fetch login.jsp instead. This is the essence of the solution being applied here.

    Listing 7. securePage.jsp

      ...
    
    <%@ include file="noCache.jsp" %>
    <%
       String userName = (String) session.getAttribute("User");
       if (null == userName) response.sendRedirect("login.jsp");
    %>
    
    <!-- Standard JSF from here down -->
    ...
    <!-- #{service.logout} is mapped to SecurityService.logout() in step 8 -->
    <h:commandLink action="#{service.logout}" >
       <h:outputText value="Logout" />
    </h:commandLink>
    ...
    
  8. Somewhere in a restricted resource, represented by securePage.jsp in this recipe, a JSF <h:commandLink> to the logout action must be provided. The action of this link is wired to SecurityService.logout(), where it invalidates the session and performs a redirect to login.jsp via another navigation rule. Listing 8 shows SecurityService.logout(), and Listing 9 shows the navigation rule.

    Listing 8. SecurityService.logout()

      ...
    public class SecurityService {
       ...   
       public String logout() {      
          SessionUtil.getSession().invalidate();
          return "logout";
       }
    }
    

    Listing 9. Navigation rule configuration for logout in faces-config.xml

      ...
    <navigation-rule>
       <from-view-id>/restrictedPages/securePage.jsp</from-view-id>
    
       <navigation-case>
       <from-outcome>logout</from-outcome>
                
          <to-view-id>/restrictedPages/login.jsp</to-view-id> 
          <redirect/>
       </navigation-case>
    </navigation-rule>
    ...
    

JSF recipe variations

Readers can alter the above JSF recipe as follows:

  1. Instead of checking the session for a user ID to discern whether the user is currently logged in, and then allow or disallow the user to access a restricted page, implement a filter and configure it in the web.xml config file. The Stripes recipe, presented next, uses this filter approach.
  2. If the user typed an incorrect user ID and/or password, instead of redirecting to an error page, namely loginError.jsp, where a back link to login.jsp is provided, the alternative is to redirect to login.jsp automatically and display the error in red, for example. This approach saves the users a mouse click. The Stripes recipe also takes this approach.

Stripes recipe

Figure 4 depicts a flow diagram in a Stripes application that handles login and logout functionality. Rectangle boxes represent Stripes ActionBean classes, and rounded rectangle boxes represent JSP pages. As expected, this flow diagram is similar to the one shown in Figure 3, but the difference is, in Figure 4, the actionBean classes, a central notion in a Stripes Web application, replace the JSF managed bean SecurityService.

Figure 4. Flow diagram of the Stripes recipe

The steps to the Stripes recipe follow:

  1. As with the JSF recipe, the entry point is the index.jsp page, which is configured in web.xml as the welcome page. The place-holder index.jsp simply performs a forward to login.jsp.
  2. In this Stripes recipe, with the exception of login.jsp and index.jsp, all other JSP resources can be "truly" restricted, which is not the case with the JSF recipe. All JSP resources under the /restricted folder of the Web application context are guarded by a <security-constraint> configuration in the web.xml config file. Listing 10 shows the <welcome-file-list> and <security-constraint> configuration.

    Listing 10. Configuration in web.xml

      ...
    <welcome-file-list>
       <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
      
    <security-constraint>
       <web-resource-collection>
          <web-resource-name> Restrict access to JSP pages</web-resource-name>
          <url-pattern>/restricted/*</url-pattern>
       </web-resource-collection>
       <auth-constraint>
          <description> With no roles defined, no access granted</description>
       </auth-constraint>
    </security-constraint>
    
  3. Listing 11 shows login.jsp in its entirety. Similar to the JSF recipe, login.jsp also includes noCache.jsp. The only difference is if an instance of ExceptionBean is present in the session, the message in the bean displays in red. The presence of an instance of ExceptionBean in the session is the consequence of a previous login error.

    Listing 11. login.jsp

      <%@ include file="restricted/includeStripes.jsp" %>
    <jsp:useBean id="exceptionBean" scope="session"
       class="org.pragmaticobjects.stripesProperLogout.bean.ExceptionBean"/>
    
    <%@ include file="restricted/noCache.jsp" %>
    
    <html>
       <head>
          <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
          <title>stripes-ProperLogout</title>
       </head>
       <body>
          <stripes:form name="signon" action="/stripes/Login.action" method="POST">
             User Id ("guest"): <stripes:text id="id" name="id" value=""/>
             <br/>
             Password ("abc123") : <stripes:password id="password" name="password" value=""/>
             <br/>
             <br/>            
             <input type="submit" name="submit" value="Login" />
             <div style="color:red;">${exceptionBean.message}</div>         
          </stripes:form>
       </body>
    </html>
    
  4. Unlike the JSF recipe, there's no

    SecurityService

    managed bean when using Stripes. In place of

    SecurityService

    is

    LoginActionBean

    , which subclasses

    BaseActionBean

    and implements Stripes's

    ActionBean

    interface. The class

    BaseActionBean

    defines a convenient method,

    setNoCache()

    , that sets the no-cache in the response header; this logic is equivalent to the noCache.jsp in the JSF recipe.

    Since Stripes is annotation-driven (there's no XML configuration to mess with), through annotation, LoginActionBean is bound to /stripes/Login.action, which is the action of the login form in Step 3. LoginActionBean's login() method creates an instance of RedirectResolution and returns it. Depending on whether the call to service.authenticate() returns true (login passes) or false (login fails), a RedirectResolution instance is created with the argument of /stripes/securePage.action or /index.jsp, respectively, and returned. Also, depending on whether login passes or fails, a user ID or an instance of ExceptionBean is placed in the session. Listings 12 and 13 show the BaseActionBean and LoginActionBean classes in their entirety.

    Listing 12. BaseActionBean

      ...
    import javax.servlet.http.HttpServletResponse;
    import net.sourceforge.stripes.action.ActionBeanContext;
    
    public class BaseActionBean {
       private ActionBeanContext context;
       
       public ActionBeanContext getContext() { return context; }
       public void setContext(ActionBeanContext context) { this.context = context; }   
       
       protected final void setNoCache(HttpServletResponse response) {
          response.setHeader("Cache-Control","no-cache"); 
          response.setHeader("Cache-Control","no-store");
          response.setDateHeader("Expires", 0);
          response.setHeader("Pragma","no-cache");
       }
    }
    

    Listing 13. LoginActionBean

      ...
    import javax.servlet.http.*;
    import net.sourceforge.stripes.action.*;
    import org.pragmaticobjects.stripesProperLogout.service.SecurityService;
    import org.pragmaticobjects.stripesProperLogout.bean.ExceptionBean;
    
    @UrlBinding("/stripes/Login.action")
    ...
    public class LoginActionBean extends BaseActionBean implements ActionBean {
       private String id;
       private String password;   
       
       @DefaultHandler @HandlesEvent("Login")
      public Resolution login() {
       HttpServletRequest request = getContext().getRequest();
       HttpSession session = request.getSession();  
    
       SecurityService service = new SecurityService(); 
       boolean loginStatus = service.authenticate(id, password);
       if (loginStatus) {  
          session.setAttribute("User", id); 
          return new RedirectResolution("/stripes/SecurePage.action");
       } else {   
          ExceptionBean exceptionBean = new ExceptionBean();
          exceptionBean.setMessage("Invalid credential.  Please log in again.");
          session.setAttribute("exceptionBean", exceptionBean);
          return new RedirectResolution("/index.jsp");
       }
    }
       
       //Setters, getters for id, password
       ...
    }
    
  5. SecurePageActionBean is another Stripes action bean. Its only method, securePage(), sets the no-cache in the response header and dispatches a forward to /restricted/securePage.jsp. SecurePageActionBean is needed because LoginActionBean.login() cannot simply perform a redirect to /restricted/securePage.jsp. Since /restricted/securePage.jsp is guarded by <security-constraint> in Step 2 above, a redirect to it will result in a "Resource not available" error message. Listing 14 shows SecurePageActionBean in its entirety.

    Listing 14. SecurePageActionBean

      ...
    import javax.servlet.http.HttpServletResponse;
    import net.sourceforge.stripes.action.*;
    
    @UrlBinding("/stripes/SecurePage.action")
    public class SecurePageActionBean extends BaseActionBean implements ActionBean {
    
       @DefaultHandler @HandlesEvent("SecurePage")
        public Resolution securePage() {
          HttpServletResponse response = getContext().getResponse();
          setNoCache(response);
          return new ForwardResolution("/restricted/securePage.jsp");
       }
    }
    
  6. Since Step 5 already ensures the pages are marked for no-cache, nothing else pertaining to the logout solution is required in securePage.jsp. However, somewhere on this page, there should be a <stripes-link> to the logout action as shown in Listing 15.

    Listing 15. securePage.jsp

      ...
    <stripes:link href="/stripes/Logout.action">Logout</stripes:link>
    ...
    
  7. LogoutActionBean.logout is equivalent to SecurityService.logout() in the JSF recipe. This is where the session is invalidated. Listing 16 shows LogoutActionBean in its entirety.

    Listing 16. LogoutActionBean

      ...
    import javax.servlet.http.*;
    import net.sourceforge.stripes.action.*;
    
    @UrlBinding("/stripes/Logout.action")
    public class LogoutActionBean extends BaseActionBean implements ActionBean {
    
       @DefaultHandler @HandlesEvent("Logout")
        public Resolution logout() {
          HttpServletRequest request = getContext().getRequest(); 
          HttpSession session = request.getSession(); 
                      
          session.invalidate();      
          return new RedirectResolution("/index.jsp"); 
       }
    }
    
  8. Finally, a security filter must be implemented and configured in the web.xml config file. Listing 17 shows SecurityFilter in its entirety. Listing 18 shows the security filter configuration in the web.xml config file.

    Listing 17. SecurityFilter

      ...
    import javax.servlet.*;
    import javax.servlet.http.*;
    import java.io.IOException;
    import java.util.*;
    
    public class SecurityFilter implements Filter {
    private static Set<String> publicUrls = new HashSet<String>();
       
       static {
          publicUrls.add("login.jsp");
          publicUrls.add("/stripes/Login.action");
       }
       
       public void init(FilterConfig filterConfig) throws ServletException { }
       public void destroy() { }
        
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
          FilterChain filterChain) throws IOException, ServletException {              
    
          HttpServletRequest request = (HttpServletRequest) servletRequest;
          HttpServletResponse response = (HttpServletResponse) servletResponse;
    
          if (request.getSession().getAttribute("User") != null) {
             filterChain.doFilter(request, response);
          } else if (isPublicResource(request)) {
             filterChain.doFilter(request, response);
          } else {          
             response.sendRedirect(request.getContextPath() + "/index.jsp");
          }
       }
        
       protected boolean isPublicResource(HttpServletRequest request) {
          String resource = request.getServletPath();
    
          return publicUrls.contains(request.getServletPath())
             || (!resource.endsWith(".jsp") && !resource.endsWith(".action"));
       }
    }
    

    Listing 18. Configuration of SecurityFilter in web.xml

      <filter>
       <description>Provides login security for stripes-ProperLogout</description>
       <filter-name>ProperLogoutSecurityFilter</filter-name>
       <filter-class>org.pragmaticobjects.stripesProperLogout.stripes.filter.SecurityFilter</filter-class>
    </filter>
    
    <filter-mapping>
       <filter-name>ProperLogoutSecurityFilter</filter-name>
       <url-pattern>/stripes/*</url-pattern>
       <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
    

Summary

This article discussed how to solve the logout problem created by a browser's Back button. In this solution, the quirk where a browser throws a confusing warning message is completely eliminated. Here is the essence of the solution I discussed:

  • Set no-cache in the response header in all restricted resources (in Stripes's action bean or, in the case of JSF, JSP files):
      response.setHeader("Cache-Control","no-cache"); 
    response.setHeader("Cache-Control","no-store"); 
    response.setDateHeader("Expires", 0); 
    response.setHeader("Pragma","no-cache");
    
  • Implement a filter and configure it in web.xml, or, in all restricted resources, check for user ID (or user bean) in the session. If null, redirect to login.jsp.
  • Perform a redirect after the login form POST.

Based on this solution, the article presented recipes for both JSF and Stripes. These recipes can be easily adapted or applied in building Web applications that properly handle logout functionality despite the behavior of a browser's Back button.

Kevin Hoang Le currently works for an oil and gas company building EAI solutions by day. By night, he contributes to the open source community by building products and libraries that are released under open source licenses. Please visit his personal Website at http://pragmaticobjects.org, where he publishes his work. He also likes to do his tiny part of contributing his ideas and solutions to benefit the Java community.

Learn more about this topic

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