Store session data in cookie

In Web applications, users session can store a lot of data:

  • Information about the users profile
  • User roles
  • Temporary data (for example, wizard step data)

However, such storage can be a real headache if we have few backend servers that serve this application. To provide load-balancing and fault-tolerance in the service, session data should be stored in external storage (redis, memcached, sql). Appearance of new storage adds stress to supporting and performance.

Another way to exchange data between the backend servers can be a client browser. For this you just need to encrypt the users session and store it in a cookie.

To serialize/deserialize session will use a standard Servlet Filter.

private void loadAttributes(HttpServletRequest req)  {
    Map<String, Object> attributesFromCookie = getSessionAttributesFromCookie(req);
    if (attributesFromCookie != null) {
        HttpSession session = req.getSession(true);
        for (Map.Entry<String, Object> entry : attributesFromCookie.entrySet()) {
            session.setAttribute(entry.getKey(), entry.getValue());
        }
    }
}

private Map<String, Object> getSessionAttributesFromCookie(HttpServletRequest req) {
    Cookie cookie = null;
    for (Cookie c : req.getCookies()) {
        if (c.getName().equals(COOKIE_NAME)) cookie = c;
    }

    Map<String, Object> attributes = null;
    if (cookie != null) {
        attributes = sessionDecrypt.decrypt(cookie.getValue());
    }
    return attributes;
}

It will load cookie attributes for each request. To avoid updating cookie on each request and eliminate the need to proxy HttpServletResponse lets create static update method.

public static void update() {
    if (requestLocalStorage.get() != null && responseLocalStorage.get() == null) {return;}
    HttpSession session = requestLocalStorage.get().getSession(false);
    if (session != null && session.getAttributeNames().hasMoreElements()) {
        Cookie cookie = new Cookie(COOKIE_NAME, sessionDecrypt.encrypt(session));
        cookie.setPath("/");
        responseLocalStorage.get().addCookie(cookie);
    }
}

The code in the controller, which updates the session:

@Controller
@RequestMapping("/")
public class HelloController {
    @RequestMapping("/")
    public String info(HttpSession session, Model model) {
        Integer counter = (Integer) session.getAttribute("counter");
        if (counter == null) {counter = 0;}
        counter++;
        session.setAttribute("counter", counter);
        model.addAttribute("counter", counter);
        update();
        return "index";
    }
}

The encrypted data contains AES algorithm with SHA-1 signature. AES krypt the serialized attributes with 16 digit key and puts it in a base64. Then add sha-1 signature to this base64.

Session decryptor:

public class SessionDecrypt {
    private static final String KEY = "1234567890123456";
    private static Logger log = LoggerFactory.getLogger(SessionDecrypt.class);

    public String encrypt(HttpSession session) {
        try {
            Cipher aes = createChiper(Cipher.ENCRYPT_MODE);
            byte[] bytes = SerializationUtils.serialize(readAttributeMap(session));
            String encryptedSession = DatatypeConverter.printHexBinary(aes.doFinal(bytes));
            String signature = calculateSignature(bytes).toUpperCase();
            return encryptedSession + signature;
        } catch (Exception e) {
            log.error("Can't encrypt session", e);
        }
        return null;
    }

    public Map<String, Object> decrypt(String session) {
        try {
            String signature = session.substring(session.length() - 40);
            String encryptedSession = session.substring(0,session.length() - 40);

            Cipher aes = createChiper(Cipher.DECRYPT_MODE);
            byte[] bytes = aes.doFinal(DatatypeConverter.parseHexBinary(encryptedSession));

            if (!signature.equals(calculateSignature(bytes).toUpperCase())) {
                log.error("Session has been tampered with");
                return null;
            }

            //noinspection unchecked
            return (Map<String, Object>) SerializationUtils.deserialize(bytes);
        } catch (Exception e) {
            log.error("Can't decrypt session", e);
        }

        return null;
    }

    private Cipher createChiper(int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
        aes.init(mode, new SecretKeySpec(KEY.getBytes(), "AES"), new IvParameterSpec(new byte[16]));
        return aes;
    }

    private Map<String, Object> readAttributeMap(HttpSession session) {
        HashMap<String, Object> result = new HashMap<>();
        Enumeration<String> attributeNames = session.getAttributeNames();
        while (attributeNames.hasMoreElements()) {
            String el = attributeNames.nextElement();
            result.put(el, session.getAttribute(el));
        }
        return result;
    }

    private String calculateSignature(byte[] serialisedSession) {
        try {
            MessageDigest cript = MessageDigest.getInstance("SHA-1");
            cript.reset();
            cript.update(serialisedSession);
            return String.format("%1$40s", new BigInteger(1, cript.digest()).toString(16));
        } catch (Exception e) {
            log.error("Can't calculate signature", e);
        }
        return null;
    }
}

Using encrypted cookie as a repository for the session you can get rid of a layer of the infrastructure. Without losing functionality.

Sample on github.

3 thoughts on “Store session data in cookie

  1. Fine. I like the idea the server to be stateless and having minimal infrastructure dependencies. So storing context on client is nice to me.

    But in this solution, the security is broken if the encript KEY (which is hard-coded) is stolen.
    Has someone a solution to overcome this issue ?
    I know that we can externalize the key in a JNDI or system properties but it still delicate to update as it requires all servers to be keep in sync.

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax