Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

AppFuse Refactorings Part III: Remember Me

This is a continuing series on what I'm doing to make AppFuse a better application in Winter/Spring 2004. Previous titles include: Changing the Directory Structure and Spring Integration.

- - - -
AppFuse includes a Remember Me feature that works with Container-Managed Authentication. In version 1.3 it works by setting a few cookies: username, password and rememberMe. The last one being a simple flag that the user wants to be remembered. Then a LoginFilter checks for the rememberMe cookie, and if present, logs in the user using the other cookie values. The obvious issue with this is that the password being sent and stored on the user's browser.

This was easily solved in Tomcat 4 by placing the form-login-page and form-error-page under a "security" directory and then setting cookies on the /appfuse/security path. This way, since no other part of the app can access /appfuse/security, these cookies can never be retrieved in any part of the application. The problem is that this didn't work in Tomcat 5 since it forwards to the login page (rather than redirecting). Since forwarding is obviously a better solution (user's can't bookmark the login page), I needed a new way to implement the Remember Me feature.

To my knowledge, cookies can only be stolen if someone is able to login to your AppFuse app and insert JavaScript to send the "document.cookie" value to an external URL. So for AppFuse, it's likely that stealing cookies is not much of an issue. However, for applications like Roller, it is an issue - since other bloggers on the same server (i.e. JRoller) could put JavaScript on their blog to grab cookies from other users.

Just as I was about to give up searching for solutions, along came Charle's persistent cookie strategy. Here's how I implemented it in AppFuse. Hopefully it follows all the rules and is a good solution. Here's what I did make it happen.

- - - -
Step 1: Setting the cookie.
Scenario: A user logs in and selects the "Remember Me" checkbox.
What Happens: When a user clicks the Login button, they submit to a LoginServlet that redirects them to "j_security_check" to take advantage of Container-Managed Authentication. This servlet is responsible for ensuring an SSL Login (if enabled), encrypting the user's password (if enabled) and also sets a session variable to indicate the user wants to be remembered. After authenticating, the user will hit the ActionFilter, where the following code sits:

    // if user wants to be remembered, create a remember me cookie
    if (session.getAttribute(Constants.LOGIN_COOKIE!= null) {
        session.removeAttribute(Constants.LOGIN_COOKIE);
        String loginCookie = mgr.createLoginCookie(username);
        RequestUtil.setCookie(response, Constants.LOGIN_COOKIE,
                              loginCookie, request.getContextPath());
    

In the above code snippet, the UserManager.createLoginCookie(username) method is responsible for creating a new cookie string and storing this information in the database.

    public String createLoginCookie(String usernamethrows Exception {
        UserCookie cookie = new UserCookie();
        cookie.setUsername(username);

        return saveLoginCookie(cookie);
    }
  
    /**
     * Convenience method to set a unique cookie id and save to database
     @param cookie
     @return
     @throws Exception
     */
    private String saveLoginCookie(UserCookie cookiethrows Exception {
        cookie.setCookieId(new RandomGUID().toString());
        dao.saveUserCookie(cookie);

        return cookie.getUsername() "|" + cookie.getCookieId();
    }

The RandomGUID is a class I found on Java Exchange. Once the rememberMe cookie was set, I had to configure LoginFilter.java (mapped to form-login-page and form-error-page) to look for this cookie. This brings us to Step 2.

- - - -
Step 2: Using the cookie to login the user.
Scenario: A User has already logged in successfully with "Remember Me" enabled.
What Happens: When the login page is served up to the user, the LoginFilter is invoked and it checks the validity of the "Remember Me" cookie.

    Cookie c = RequestUtil.getCookie(request, Constants.LOGIN_COOKIE);

    WebApplicationContext context = 
        (WebApplicationContextconfig.getServletContext().getAttribute
        (WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    UserManager mgr = (UserManagercontext.getBean("userManager");
  
    if (c != null) {
        try {
            String loginCookie = mgr.checkLoginCookie(c.getValue());

The UserManager.checkLoginCookie(value) method looks up a record based on the random GUID, and if it finds a match, it creates a new GUID and saves it in the database. If null is returned, it means the cookieId doesn't exist, and the login proceeds as it normally would. Below is the guts of the checkLoginCookie() method.

    public String checkLoginCookie(String valuethrows Exception {

        String[] values = StringUtils.split(value, "|");

        if (log.isDebugEnabled()) {
            log.debug("looking up cookieId: " + values[1]);
        }

        UserCookie cookie = dao.getUserCookie(values[1]);

        if (cookie != null) {
            if (log.isDebugEnabled()) {
                log.debug("cookieId lookup succeeded, generating new cookieId");
            }

            return saveLoginCookie(cookie);
        else {
            if (log.isDebugEnabled()) {
                log.debug("cookieId lookup failed, returning null");
            }

            return null;
        }
    }

You can see from this, that if the lookup succeeds - a new cookieId is saved and returned. If a not-null cookieId is returned, the remember me cookie is updated, the user is looked up and the Filter forwards an authentication request (with username/password) to the LoginServlet. The Filter also sets an attribute to let the application know that this user authenticated via cookies. This is important so that cookie-authenticated users cannot change passwords. When using cookie-authentication, the password field is hidden and a message warns the user that they must logout/login to change passwords.

Lastly, I had to come up with a solution to remove these login cookies.

- - - -
Step 3: Allow the user to clear their login cookies.
Scenario: A User has already logged in successfully with "Remember Me" enabled.
What Happens: For this, I implemented a simple solution. When a user logs out, all persistent login cookies are removed.

I don't know if it's best to divulge the details of AppFuse's cookie login strategy. However - it *is* open source - so folks can find figure it out if they really want to. By exposing it to the world, I hope to get the most robust solution possible.

Next up, how I replaced Hibernate with iBatis. Using Spring, it only took me a few hours! Pretty slick, eh? ;-)

Posted in Java at Feb 10 2004, 10:35:37 AM MST 4 Comments
Comments:

So when are you gonna implement that new Remember Me scheme for Roller?

Posted by Lance on February 10, 2004 at 02:35 PM MST #

It might be awhile, my <em>to do list</em> is pretty long right now. Hopefully before the end of the month and the next release.

Posted by Matt Raible on February 10, 2004 at 03:54 PM MST #

Just my 2 cents : you say "When using cookie-authentication, the password field is hidden and a message warns the user that they must logout/login to change passwords." - an alternative approach would be to require a user to /always/ enter their old password on the 'Enter your new password screen'. Although it's a bit anal retentive, maybe that way is a bit simpler and also a bit more secure? Roberto

Posted by Roberto on February 12, 2004 at 04:19 AM MST #

You could use the JDK1.5 classes now for the GUIDs... check in the java.util package I believe. They are probably a bit bigger than required (128 bits), but you could still trim them. I prefer using java packages instead of outside stuff, so it is something to think about for a jdk1.5 release...

Posted by Scott McClure on March 01, 2004 at 10:30 PM MST #

Post a Comment:
Comments are closed for this entry.