The Java EE 6 Example - Enhance Security with Galleria - Part 5

Markus Eisele
2
The previous posts (Part1 | Part 2 | Part 3 | Part 4) about the Galleria example guided you through the basics and the initial deployment to both GlassFish and WebLogic. From today on I try to add some enterprise grade features to it as I have seen them requested a lot in my own projects. I know Vineet is also going to add more features over time and I hope this is not going to be too confusing for the readers. But let's see how this works out and which of my features get adopted by Vineet and which not :). Let me know if there is anything special you would like to see added!

Session Fixation
The hottest topic for Enterprise Java applications out there is security. And because it has so many different aspects I decided to start with a very simply but often required feature: Session Fixation prevention. This isn't very Java or JSF specific but a general problem for web based applications.  Session fixation arises when session IDs are easy to discover or guess. The main method of attack is when the session ID is present in the URL or any other part of the response. An attacker could capture a session and then embed the link in their page, tricking a user into visiting it and becoming part of their session. Then when the user authenticates the session is authenticated. Using Cookies only gives a certain security here because the are most often also set via a method which implies confidentiality lose. Most application servers generate a new session ID with the first request. After this is authenticated it is re-used further on. The only way to prevent this is to issue a new, random session after a successful  authentication request.
This is easy to do in general. Go to the galleria-jsf project and find the info.galleria.view.user.Authenticator bean. Add the following lines to the beginning of the authenticate() method:
String result = null;
ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();

// Session Fixation Prevention
HttpSession session = (HttpSession) externalContext.getSession(false);

if (logger.isDebugEnabled()) {
       logger.debug("Session before authentication request: " + session.getId());
   }

session.invalidate();
session = (HttpSession) externalContext.getSession(true);

  if (logger.isDebugEnabled()) {
       logger.debug("Session after authentication request: " + session.getId());
   }
That was it. Very easy change for the first time touching the code-base. Switching to debug level FINE for package info.galleria should reveal the magic in the logfile:
[#|2012-03-27T17:17:25.298+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator|_ThreadID=27;_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName=authenticate;|Session before authentication request: 33b1205d7ad740631978ed211bce|#]

[#|2012-03-27T17:17:25.301+0200|FINE|glassfish3.1.2|info.galleria.view.user.Authenticator|_ThreadID=27;_ThreadName=Thread-4;ClassName=info.galleria.view.user.Authenticator;MethodName=authenticate;|Session after authentication request: 33b1f344ad1730c69bccc35e752e|#]
As expected we changed the http session during the authentication request. You can also check this with a browser add-on of your choice (in this case "Edit This Cookie"):


And the Galleria application got a bit securer by doing this. If you want to learn more about Session Fixation give the OWASP page a read.

Prevent Multiple Logins
The next requirement is a bit more complex. I have seen this a couple of times and even if it is inconvenient for the user it could be necessary for security reasons. As you might have guessed, there isn't a single switch for that. You have to hold a map of sessions and check if a user is already logged in or not. It should be checked during the login-process and a meaningful error message should be displayed.
There are some tricky parts in that. First one is, that you need a way to store all your user and HttpSession information for the application. And second one is, that you need a someone to look after it. Let's start with the latest.
You are in need of the famous Singleton here. A single place to store the relevant HttpSession information. First thought would be to use the .getExternalContext().getApplicationMap(). This could work. The login restriction we are placing here has some side-effects. Imagine a user loged-in and crashing his/her browser without doing a logout before. He/she would end up with not being able to login again until some cleanup or application restart happens. So it is crucial to also have access to it in a HttpSessionListener. Given the fact, that the JSF ExternalContext is the ServletContext we are safe here.
Before proceeding one more word about clustering. We are going to build a non-clusterable construct here. According to the servlet specification, context attributes are local to the JVM in which they were created. So you will lose protection if you run this in a clustered environment because you can have a session on every single node of the cluster. Making this cluster safe would mean to use either the database, an ejb component or a distributed cache.

Go to info.galleria.view.util and create a new final class with the name SessionConcierge. It needs methods for adding and removing a session. And we obviously need something to handle the application map. Starting with the addSession method which will be called from the info.galleria.view.user.Authenticator managed bean later on:
 public static boolean addSession(HttpSession session) {
        String account = FacesContext.getCurrentInstance().getExternalContext().getRemoteUser();
        String sessionId = session.getId();
        if (account != null && !getApplicationMap(session).containsKey(account)) {
            getApplicationMap(session).put(account, sessionId);
            if (logger.isDebugEnabled()) {
                logger.debug("Added Session with ID {} for user {}", sessionId, account);
            }
            return true;
        } else {
            logger.error("Cannot add sessionId, because current logged in account is NULL or session already assigned!");
            return false;
        }
    }

This basically checks if we have a loged-in user here and if the user already has a session assigned. If there is a user and he does not have a session in use already we are going to add the current session to the application map under the account as a key. Next a bit remove logic:
 public static void removeSession(HttpSession session) {
        String sessionId = session.getId();
        String account = getKeyByValue(getApplicationMap(session), sessionId);
        if (account != null) {
            getApplicationMap(session).remove(account);
            if (logger.isDebugEnabled()) {
                logger.debug("Removed Session with ID {} for user {}", sessionId, account);
            }
        }
    }
This is a bit more tricky. You noticed, that I use the account as a key for binding the session in the map. So I have to trick around a bit to invert the map and find a key by a value. This little magic happens here:
    private static <T, E> T getKeyByValue(Map<T, E> map, E value) {
        for (Entry<T, E> entry : map.entrySet()) {
            if (value.equals(entry.getValue())) {
                return entry.getKey();
            }
        }
        return null;
    }
Done. One thing is missing. The getApplicationMap(HttpSession session) method. This isn't very magic. It simply tries to figure out if we need to get it via the FacesContext or the ServletContext. Look at the SessionConcierge source if you are curious. Final thing to do is to add the SessionConcierge to the Authenticator. Add this code into the try{request.login()} (I added the first two lines for your orientation:
 request.login(userId, new String(password));
            result = "/private/HomePage.xhtml?faces-redirect=true";

            // save sessionId to disable multiple sessions per user
            if (!SessionConcierge.addSession(session)) {
                request.logout();
                logger.error("User {} allready logged in with another session", userId);
                FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, Messages.getString(
                        "Login.AllreadyLoggedIn", locale), null);
                FacesContext.getCurrentInstance().addMessage(null, facesMessage);
            }
If the addition of the HttpSession via the SessionConcierge isn't successful, the user is loged-out immediately and a FacesMessage is added. Remember to add this to the galleria-jsf\src\main\resources\resources messages.properties and it's translations. And don't forget to add the
SessionConcierge.removeSession(session);
to public String logout(). Fine. That's all, isn't it? At least it is working for now. But we still have to address those crashing browser issue. If someone isn't doing a logout via the application, the session times out or the browser crashes you will not be able to login again until the application is restarted. That is weird and unintended. Some mechanism for cleaning up is needed. What about a HttpSessionListener? That sounds great! Add it to info.galleria.listeners and call it SessionExpirationListener.
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        HttpSession session = se.getSession();

        SessionConcierge.removeSession(session);

        if (logger.isDebugEnabled()) {
            logger.debug("Session with ID {} destroyed", session.getId());
        }
    }
Fine. That should be working now. Go ahead and give it a try. Open two different browsers and try to login with both. Only one will let you access the application. The second should respond with the error message you put into the messages.properties. Please note, that this isn't a multiple window prevention. You are still free to open as many windows per HttpSession as you like.
One small addition: If you rely heavily on the HttpSessionListener cleanup you should make sure to have a right lifetime for it. It is configured via the product specific web-app deployment descriptor (e.g. weblogic.xml or glassfish-web.xml). I recommend to set it to a reasonable low value (e.g. 30 minutes or less) to not let the users wait for too long. Here is how this would look like for Glassfish (glassfish-web.xml):
 
  <session-config>
        <session-properties>
            <property name="timeoutSeconds" value="1800" />
        </session-properties>
    </session-config>
and for WebLogic (weblogic.xml)
  <session-descriptor>
        <timeout-secs>180</timeout-secs>
  </session-descriptor>
Curious about the error-handling of the Galleria example? Read on.

Post a Comment

2Comments

  1. Hi there, congrats for the post, now the question:

    what's the advantage of checking the map of sessions to prevent multiple logins over a filter that checks if user is already authenticated and if so the redirect to home page?

    ReplyDelete
    Replies
    1. Hi Raziel,

      thanks for the comment. As always, there are different ways to achieve your goals. It highly depends on the requirements. With your filter proposal you would only be able to have _one_ browser window at all. With the session tracking approach you would be able to still have multiple windows (for e.g. pop-ups).

      Thanks,
      keep the questions coming!
      -M

      Delete
Post a Comment