jakarta-eeforms-authenticationstay-logged-in

How to implement "Stay Logged In" when user login in to the web application


On most websites, when the user is about to provide the username and password to log into the system, there's a checkbox like "Stay logged in". If you check the box, it will keep you logged in across all sessions from the same web browser. How can I implement the same in Java EE?

I'm using FORM based container managed authentication with a JSF login page.

<security-constraint>
    <display-name>Student</display-name>
    <web-resource-collection>
        <web-resource-name>CentralFeed</web-resource-name>
        <description/>
        <url-pattern>/CentralFeed.jsf</url-pattern>
    </web-resource-collection>        
    <auth-constraint>
        <description/>
        <role-name>STUDENT</role-name>
        <role-name>ADMINISTRATOR</role-name>
    </auth-constraint>
</security-constraint>
 <login-config>
    <auth-method>FORM</auth-method>
    <realm-name>jdbc-realm-scholar</realm-name>
    <form-login-config>
        <form-login-page>/index.jsf</form-login-page>
        <form-error-page>/LoginError.jsf</form-error-page>
    </form-login-config>
</login-config>
<security-role>
    <description>Admin who has ultimate power over everything</description>
    <role-name>ADMINISTRATOR</role-name>
</security-role>    
<security-role>
    <description>Participants of the social networking Bridgeye.com</description>
    <role-name>STUDENT</role-name>
</security-role>

Solution

  • Java EE 8 and up

    If you're on Java EE 8 or newer, put @RememberMe on a custom HttpAuthenticationMechanism along with a RememberMeIdentityStore.

    @ApplicationScoped
    @AutoApplySession
    @RememberMe
    public class CustomAuthenticationMechanism implements HttpAuthenticationMechanism {
    
        @Inject
        private IdentityStore identityStore;
    
        @Override
        public AuthenticationStatus validateRequest(HttpServletRequest request, HttpServletResponse response, HttpMessageContext context) {
            Credential credential = context.getAuthParameters().getCredential();
    
            if (credential != null) {
                return context.notifyContainerAboutLogin(identityStore.validate(credential));
            }
            else {
                return context.doNothing();
            }
        }
    }
    
    public class CustomIdentityStore implements RememberMeIdentityStore {
    
        @Inject
        private UserService userService; // This is your own EJB.
        
        @Inject
        private LoginTokenService loginTokenService; // This is your own EJB.
        
        @Override
        public CredentialValidationResult validate(RememberMeCredential credential) {
            Optional<User> user = userService.findByLoginToken(credential.getToken());
            if (user.isPresent()) {
                return new CredentialValidationResult(new CallerPrincipal(user.getEmail()));
            }
            else {
                return CredentialValidationResult.INVALID_RESULT;
            }
        }
    
        @Override
        public String generateLoginToken(CallerPrincipal callerPrincipal, Set<String> groups) {
            return loginTokenService.generateLoginToken(callerPrincipal.getName());
        }
    
        @Override
        public void removeLoginToken(String token) {
            loginTokenService.removeLoginToken(token);
        }
    
    }
    

    You can find a real world example in the Java EE Kickoff Application.


    Java EE 6/7

    If you're on Java EE 6 or 7, homegrow a long-living cookie to track the unique client and use the Servlet 3.0 API provided programmatic login HttpServletRequest#login() when the user is not logged-in but the cookie is present.

    This is the easiest to achieve if you create another DB table with a java.util.UUID value as PK and the ID of the user in question as FK.

    Assume the following login form:

    <form action="login" method="post">
        <input type="text" name="username" />
        <input type="password" name="password" />
        <input type="checkbox" name="remember" value="true" />
        <input type="submit" />
    </form>
    

    And the following in doPost() method of a Servlet which is mapped on /login:

    String username = request.getParameter("username");
    String password = hash(request.getParameter("password"));
    boolean remember = "true".equals(request.getParameter("remember"));
    User user = userService.find(username, password);
    
    if (user != null) {
        request.login(user.getUsername(), user.getPassword()); // Password should already be the hashed variant.
        request.getSession().setAttribute("user", user);
    
        if (remember) {
            String uuid = UUID.randomUUID().toString();
            rememberMeService.save(uuid, user);
            addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE);
        } else {
            rememberMeService.delete(user);
            removeCookie(response, COOKIE_NAME);
        }
    }
    

    (the COOKIE_NAME should be the unique cookie name, e.g. "remember" and the COOKIE_AGE should be the age in seconds, e.g. 2592000 for 30 days)

    Here's how the doFilter() method of a Filter which is mapped on restricted pages could look like:

    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) res;
    User user = request.getSession().getAttribute("user");
    
    if (user == null) {
        String uuid = getCookieValue(request, COOKIE_NAME);
    
        if (uuid != null) {
            user = rememberMeService.find(uuid);
    
            if (user != null) {
                request.login(user.getUsername(), user.getPassword());
                request.getSession().setAttribute("user", user); // Login.
                addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
            } else {
                removeCookie(response, COOKIE_NAME);
            }
        }
    }
    
    if (user == null) {
        response.sendRedirect("login");
    } else {
        chain.doFilter(req, res);
    }
    

    In combination with those cookie helper methods (too bad they are missing in Servlet API):

    public static String getCookieValue(HttpServletRequest request, String name) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (name.equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
    
    public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }
    
    public static void removeCookie(HttpServletResponse response, String name) {
        addCookie(response, name, null, 0);
    }
    

    Although the UUID is extremely hard to brute-force, you could provide the user an option to lock the "remember" option to user's IP address (request.getRemoteAddr()) and store/compare it in the database as well. This makes it a tad more robust. Also, having an "expiration date" stored in the database would be useful.

    It's also a good practice to replace the UUID value whenever the user has changed its password.


    Java EE 5 or below

    Please, upgrade.