56

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>
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Thang Pham
  • 38,125
  • 75
  • 201
  • 285

4 Answers4

113

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.

Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • Fantastic, I will try to implement tonight. Thank you BalusC. – Thang Pham Feb 22 '11 at 22:05
  • @BalusC: When I use JDBCrealm provided by Glassfish, my form use `j_security_check` (which I suppose it is a servlet somewhere), look like this, `
    `, ``, ``, then inside my web.xml I would specify the realm name which I set up in glassfish. It seems that you suggest that I should create my own servlet, I am afraid it might be a security risk. Because in GF, i specify the realm to use SHA-256 to encrypt the username and password. Can I from `login` servlet, invoke `j_security_check`?
    – Thang Pham Feb 24 '11 at 01:36
  • You could just hash it with SHA-256 yourself and then pass to `login()` method. – BalusC Feb 24 '11 at 01:45
  • Thank you. In your code, `request.login(username, password);`, does request the `HttpServletRequest`? What does `request.login(username, password);` do? – Thang Pham Feb 24 '11 at 01:51
  • 1
    Yes, it is and it logs in the user the same way as `j_security_check` does. This is new in Servlet 3.0. See also "Programmatic login" in JEE6 tutorial: http://download.oracle.com/javaee/6/tutorial/doc/gjiie.html – BalusC Feb 24 '11 at 01:52
  • So I was successfully hash the password, and retrieve the correct user with the username and password, but when I do `request.long(username, password)` it throws an ServletException at me: `javax.servlet.ServletException: javax.servlet.ServletException: Exception thrown while attempting to authenticate for user: xxx`. Any idea BalusC? – Thang Pham Feb 24 '11 at 04:41
  • I figure out why. I specify the realm to hash the password. But when I receive the password on the servlet, I manually hash it, and do request.log(username, password), the the realm hash the password again. :D – Thang Pham Feb 24 '11 at 15:32
  • Hi BalusC, when you said the CookieName should be a unique cookie name, do you mean unique every time the user login (like a uuid) or something like `remember` is fine? – Thang Pham Feb 24 '11 at 16:29
  • Sorry for ambiguity, I meant the name of the cookie containing the unique ID. Just "remember", "user" or something is indeed fine. As long as it does not conflict the name of any existing cookies which are involved in the webapp :) – BalusC Feb 24 '11 at 16:31
  • 1
    Sorry for asking some many question. Just want to completely understand the code of yours. Assume that the new table the u suggest me to create call `UserSession`. Inside the `else` that indicate the user dont want to `stay logged in`, you have `rememberDAO.delete(user);`If the user dont want us to remember them, meaning we never put their entry(their userId and the uuid) inside `UserSession`, why do we need to remove them off the `UserSession`. And shouldnt `rememberDAO.delete(user);` be `rememberDAO.delete(uuid);` since uuid is the PK of the table – Thang Pham Feb 24 '11 at 17:32
  • 2
    It's just for the case that the enduser's has any previous cookies, but decided to disable the "remember me" later on. And no, `uuid` is inappropriate here since it doesn't exist in the table. Just delete all entries associated with user ID. – BalusC Feb 24 '11 at 17:38
  • Thank you. I cant seems to find @ServletFilter in my javax.servlet.annotation. Do you know what could be the reason? I am using GF3.0.1 with Java EE6, I must have Servlet3.0, correct? Is there a way to check the version of my servlet? – Thang Pham Feb 24 '11 at 18:16
  • Does `urlPatterns` catch the GET request as well? It only get into my Filter when I do a sendRedirect("..."), but when I type the URL on the web address, it does not. I post some code on my original post, can u help me take a look at it? – Thang Pham Feb 24 '11 at 20:16
  • It should do. The `sendRedirect()` is effectively also a GET request. Note that the URLs are context-relative and should match the exact request URI as you've typed in the address bar. – BalusC Feb 24 '11 at 21:00
  • Yeah, I figure it out.Before when I left all the authentication to the container, I configure in my web.xml. When I take out , then my Filter work correctly. So does my Filter is a way to replace in my web.xml? – Thang Pham Feb 24 '11 at 21:36
  • Hmm, there you say something. I expected that the Filter would just work in combination with the constraint. It was not my intent to replace it with the Filter. But in theory you *could* do. – BalusC Feb 24 '11 at 21:41
  • Hmmm, that just weird then. give me an easy way to map certain urlPattern to be accessed only by certain Role. I guess you can do that will Filter, but I just hate the overhead generate by Filter :( – Thang Pham Feb 24 '11 at 21:51
  • I try to search around for known problem between Filter and and there arent seem to be any. I post portion of my web.xml that are relate between Filter and . can you take a look at them, to see if u spot any wrong doing? – Thang Pham Feb 24 '11 at 22:05
  • Hi BalusC. I kind of have a off topics question, and I hope you spare some times to answer. Your FileServlet implementation on your blog teach me how to implement Expire header: `response.setDateHeader("Expires", System.currentTimeMillis() + DEFAULT_EXPIRE_TIME`, should I have this code inside my LoginServet (above)? Second, On your blog talk about web performance, when u talk `Use Query String with a timestamp to force re-request`, you create a ServletContextListener, your comment said to put this code in Application Scoped, is it like Application Scoped bean? – Thang Pham Feb 25 '11 at 03:13
  • The security constraint can block the request to the filter. You should then end up in the login page, right? Or does the filter not get invoked at all when you're already logged in? As to the expires header, you don't need this on dynamic resources. You'd like to disable cache for them. And yes, it can perfectly be an application scoped managed bean. Even more, you can declare `java.util.Date` as an application scoped `` in `faces-config` with name `startupTime` or something so that you can reference it by `#{startupTime}` (and `#{startupTime.time}` would return time in millis). – BalusC Feb 25 '11 at 03:22
  • Sorry, BalusC, I was out of town for the weekend. I think the security constraint did block the request to the filter. When I first login, it actually invoke the Filter. Then after that, the filter does not get invoke anymore, so anywhere I go, I always get redirect back to the login page. As for the Expire header, I do load quite a few static resources (picture) on my page. Where should I set my Expire header, BalusC? Should I disable cache on dynamic content inside the Filter that you showed me above? – Thang Pham Feb 28 '11 at 15:54
  • @BalusC: I reread your blog and what you said above. I think I start to understand more. You use `ServletContextListner#contextInitialized` to set up the `startTime`,and this will only get invoke once when the application start. Am I correct? For the Expire tag, I now understand that I need to put it to whichever servlet handle static resource. Now, I actually did not load the images using a Servlet, I create Virtual Directory in GF (from your suggestion), and load the images using . Any idea on how set expire header here or do I have to load the image from a servlet? – Thang Pham Feb 28 '11 at 17:25
  • Inside doFilter, when you check `if(user != null)`, the line `request.getSession().setAttribute("user", user);` generate an exception: `Cannot find message associated with key standardSession.bindingEvent java.lang.IllegalStateException: PWC2776: invalidate: Session already invalidated`. Any idea why, BalusC? Let me know if u want to see all the stack trace. – Thang Pham Mar 01 '11 at 01:34
  • `session.invalidate()` was been called prior this line. You cannot access any session attributes then. You need to let the client fire a new request by `sendRedirect()` and then try again (or just to fix the code that it doesn't unnecessarily invalidate the session, if the code was doing that). – BalusC Mar 01 '11 at 01:42
  • The only place that I have `session.invalidate` is when I log out. I double check to make sure that I did not `session.invalidate`. My code is very similar to the one you provide me. I will try to walk through a debugger to see when the session get invalidated. In the mean time, can anything else invalidate the session. – Thang Pham Mar 01 '11 at 02:14
  • Not sure if this is a bug, but when I walk through the debugger, before I do `getSession().setAttribute(...)`, I look at the session and there are 5 attributes in it, and after the setAttribute, the `user` attribute is in the session attributes list. But inside an error log, there are still exception saying that `Session already got invalid`. – Thang Pham Mar 01 '11 at 02:40
  • Please help me one more time. So when I log in without check the `remember me`, I set the session attribute, and I log in. That work great. When I login, from my Session Scoped Managed, I still able to obtain the user object from the `session.getAttribute("user")`. But after that, any request will bring me back to log in page. I use debugger and see that in my session no longer have attribute "user". What weird is that, I saw in my session, the attribute for my `Session Scoped Managed Bean` which still hold the valid `user` object. I set session time out to be 30 min. Any idea why? – Thang Pham Mar 01 '11 at 04:16
  • Not sure if it is related to the cookie issue, but I forgot a `request.login(user.getUsername(), user.getPassword());` line in the filter. See the updated code snippet. Otherwise the `` will bring you back to the default login page. Maybe Glassfish is then also invalidating the session. – BalusC Mar 01 '11 at 16:08
  • I add `request.login(user.getUsername(), user.getPassword());` in my Filter, but it does not fix my latest problem, in which I lose my session attribute. I figure out that, between the request, my session id is not the same. I post this in a separate post, if u have some spare, will u take a look? http://stackoverflow.com/questions/5156942/javaee-losing-session-attribute – Thang Pham Mar 01 '11 at 17:42
  • Any strong reasons why use an UUID instead of the user password in the cookier? – Marcio Aguiar Apr 13 '12 at 18:28
  • 3
    @Marcio: cookies can be stolen, exposed or tampered by various means, either accidently or awarely. You should never store sensitive user-specific information like username and password in cookies. An UUID just ensures an unique and unguessable value which is at its own worthless information. – BalusC Apr 13 '12 at 18:33
  • Does anyone have a complete working example? I do understand the code except for the "request.login(user.getUsername(), user.getPassword());". What does it do? How to configure such a realm that it uses (Tomcat)? Google didn't give me many tutorials. – AndrewBourgeois May 29 '12 at 13:29
  • @BalusC: the remember checkbox returns "on" to me when checked, so wouldn't it be better to just check for "!= null" (boolean remember = request.getParameter("remember") != null;)? – AndrewBourgeois May 30 '12 at 17:06
  • 1
    @Andrew: That's the (browser-specific) default value when you don't specify `value` attribute on ``. So if you've changed it like that, then you indeed need to take that into account. – BalusC May 30 '12 at 17:10
  • @Andrew: as to Tomcat realm, check Tomcat docs: http://tomcat.apache.org/tomcat-7.0-doc/realm-howto.html – BalusC May 30 '12 at 17:12
  • 2
    I'm really sorry to restart this old discussion, but i have one question @BalusC: do you store the password in clear text in the user object? (It seems you must, as you call login(...user.getPassword()); How is this represented in your persistance? – bmurauer Jul 19 '13 at 12:05
  • @ThangPham "j_security_check (which I suppose it is a servlet somewhere)" it's not a Servlet. Often it's a single security module (JASPIC or container specific) that is invoked before any Filter or Servlet and just inspects every request URL. If it's j_security_check it does something otherwise it invokes the Filter/Servlet chain (if allowed) – Mike Braun Feb 03 '14 at 09:49
  • 3
    If you combine @BalusC's answers for a lot of questions, it'll become a book to must have on your shelf, surely. – Swapnil Aug 25 '14 at 05:41
  • @BalusC, but HttpServletRequest.login takes plain password, but when I fetch User from database it contains encrypted password. – guest May 31 '16 at 18:49
  • @BalusC but what happens if the user doesn't log off via the application, instead waiting for both session and cookie to expire? Then the entry for this user in rememberDAO will never be deleted and, in the limit, you could get an overflow for remembering so many users that are now unable to log off because they lost their connection to the webapp. – Douglas De Rizzo Meneghetti Sep 05 '16 at 14:30
  • @Douglas: just let a background job delete expired entries. – BalusC Sep 05 '16 at 17:21
  • @BalusC then you'd have to save the time the object was inserted in the rememberDAO, in order to know which ones are there for a long time. – Douglas De Rizzo Meneghetti Sep 05 '16 at 17:49
  • @BalusC, so you suggest to use a filter to transparently login users. Am I right that in this case I will not be able to use security-constraint sections in web.xml to controll role-based access to urls, because security-constraint with auth-constraint will be applied before the filter, and transparent login will not work? – BlindNW Aug 23 '18 at 16:31
22

Normally this is done like this:

When you log in a user you also set a cookie on the client ( and store the cookie value in the database ) expiring after a certain time (1-2 weeks usually).

When a new request comes in you check that the certain cookie exists and if so look into the database to see if it matches a certain account. If it matches you will then "loosely" log in that account. When i say loosely i mean you only let that session read some info and not write information. You will need to request the password in order to allow the write options.

This is all that is. The trick is to make sure that a "loosely" login is not able to do a lot of harm to the client. This will somewhat protect the user from someone who grabs his remember me cookie and tries to log in as him.

Mihai Toader
  • 12,041
  • 1
  • 29
  • 33
4

You cannot login a user completely via HttpServletRequest.login(username, password) since you shouldn't keep both username and plain text password in the database. Also you cannot perform this login with a password hash which is saved in the database. However, you need to identify a user with a cookie/DB token but log him/her in without entering password using custom login module (Java class) based on Glassfish server API.

See the following links for more details:

http://www.lucubratory.eu/custom-jaas-realm-for-glassfish-3/

Custom Security mechanism in Java EE 6/7 application

Community
  • 1
  • 1
John Mikic
  • 640
  • 6
  • 11
0

Although the answer by BalusC (the part for Java EE 6/7) gives useful hints, I doesn't work in modern containers, because you can't map a login filter to pages that are protected in a standard way (as confirmed in the comments).

If for some reason you can't use Spring Security (which re-implements the Servlet Security in an incompatible way), then it's better to stay with <auth-method>FORM and put all the logic into an active login page.

Here's the code (the full project is here: https://github.com/basinilya/rememberme )

web.xml:

    <form-login-config>
        <form-login-page>/login.jsp</form-login-page>
        <form-error-page>/login.jsp?error=1</form-error-page>
    </form-login-config>

login.jsp:

if ("1".equals(request.getParameter("error"))) {
    request.setAttribute("login_error", true);
} else {
    // The initial render of the login page
    String uuid;
    String username;

    // Form fields have priority over the persistent cookie

    username = request.getParameter("j_username");
    if (!isBlank(username)) {
        String password = request.getParameter("j_password");

        // set the cookie even though login may fail
        // Will delete it later
        if ("on".equals(request.getParameter("remember_me"))) {
            uuid = UUID.randomUUID().toString();
            addCookie(response, COOKIE_NAME, uuid, COOKIE_AGE); // Extends age.
            Map.Entry<String,String> creds =
                    new AbstractMap.SimpleEntry<String,String>(username,password);
            rememberMeServiceSave(request, uuid, creds);
        }
        if (jSecurityCheck(request, response, username, password)) {
            return;
        }
        request.setAttribute("login_error", true);
    }

    uuid = getCookieValue(request, COOKIE_NAME);
    if (uuid != null) {
        Map.Entry<String,String> creds = rememberMeServiceFind(request, uuid);
        if (creds != null) {
            username = creds.getKey();
            String password = creds.getValue();
            if (jSecurityCheck(request, response, username, password)) {
                return; // going to redirect here again if login error
            }
            request.setAttribute("login_error", true);
        }
    }
}

// login failed
removeCookie(response, COOKIE_NAME);
// continue rendering the login page...

Here's some explanation:

Instead of calling request.login() we establish a new TCP connection to our HTTP listener and post the login form to the /j_security_check address. This allows the container to redirect us to the initially requested web page and restore the POST data (if any). Trying to obtain this info from a session attribute or RequestDispatcher.FORWARD_SERVLET_PATH would be container-specific.

We don't use a servlet filter for automatic login, because containers forward/redirect to the login page BEFORE the filter is reached.

The dynamic login page does all the job, including:

  • actually rendering the login form
  • accepting the filled form
  • calling /j_security_check under the hood
  • displaying login errors
  • automatic login
  • redirecting back to the initially requested page

To implement the "Stay Logged In" feature we save the credentials from the submitted login form in the servlet context attribute (for now). Unlike in the SO answer above, the password is not hashed, because only certain setups accept that (Glassfish with a jdbc realm). The persistent cookie is associated with the credentials.

The flow is the following:

  • Get forwarded/redirected to the login form
  • If we're served as the <form-error-page> then render the form and the error message
  • Otherwise, if some credentials are submitted, then store them and call /j_security_check and redirect to the outcome (which might be us again)
  • Otherwise, if the cookie is found, then retrieve the associated credentials and continue with /j_security_check
  • If none of the above, then render the login form without the error message

The code for /j_security_check sends a POST request using the current JSESSIONID cookie and the credentials either from the real form or associated with the persistent cookie.

basin
  • 3,949
  • 2
  • 27
  • 63