Extended Cookie Implementation for HttpOnly Cookies

package com.developi.openntf;

/**
 * Extended Cookie Snippet v1.00: 
 * (C) 2013 Serdar Basegmez 
 * 
 * This class is a derivative work of SimpleCookie class from Apache Shiro 
 * project licensed under Apache License V2.0. 
 * 
 * Apache Shiro 
 * Copyright 2008-2012 The Apache Software Foundation 
 * 
 * This product includes software developed at 
 * The Apache Software Foundation (http://www.apache.org/). 
 * 
 * Certain parts (StringUtils etc.) of the source code for this 
 * product was copied for simplicity and to reduce dependencies 
 * from the source code developed by the Spring Framework Project 
 * (http://www.springframework.org). 
 * 
 */

import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.ibm.commons.util.StringUtil;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class CookieEx extends Cookie {

	/**
	 * Usage:
	 * 
	 * This snippet contains a Java class extending the standard Cookie of J2EE. The problem
	 * of current JRE we are using in XPages is that it doesn't support HttpOnly cookies. 
	 * HttpOnly cookies are very important for security practices. In summary, if you mark a 
	 * cookie as HttpOnly, browser will not let the client-side JavaScript code to access the
	 * value of that cookie. So it's a suggested method to protect the content of a cookie 
	 * against possible client-side attack such as XSS or malicious browser plugins.
	 * 
	 * I have also seen many XPages developers having difficulties on removing cookies. To 
	 * remove a cookie, you should add an expired cookie to the servlet response. This class 
	 * contains an helper method to ease cookie removal.
	 * 
	 * To add a new cookie:
	 * 
	 * CookieEx c = new CookieEx("Test", "SomeValue");  // Create a cookie 'Test=SomeValue'
	 * c.setPath("/");  // Set its path. By default, it will be the relative path of your app.
	 * c.setMaxAge(120); // Set its max-age in seconds
	 * c.setSecure(true); // Set the cookie secure so it will only be submitted over HTTPS
	 * c.setHttpOnly(true);  // Set the cookie HttpOnly so it will not be accessible from CSJS
	 * c.save(); // Save... It will be added to the response stream.
	 * 
	 * 
	 * To remove a cookie - Method 1:
	 * 
	 * CookieEx.removeCookie("Test");  // Remove the cookie with the name 'Test' and default path. 
	 * CookieEx.removeCookie("Test", "/");  // Remove the cookie with the name 'Test' and path '/' 
	 * 
	 * To remove a cookie - Method 2:
	 * 
	 * CookieEx c=new CookieEx("Test"); // Create a cookie with name 'Test' (you may have the cookie)
	 * c.setPath("/");	// Set its path. Don't forget if you are not using the default path.
	 * c.remove(); 	// Remove... It will add an empty cookie with zero-age and browser will remove it.
	 * 
	 * 
	 * Remember, server side components cannot know the incoming cookies' path or domain values.
	 * So you should explicitly give the correct path if it's not the default one.
	 * 
	 * No need to say these methods works only for XPages. Because it's using FacesContext to get the
	 * servlet request and response object. But there are also methods with appropriate parameters. 
	 * So if you want to use it for servlets or plugins, you can remove methods commented as 
	 * 'XPages Specific' and use the following ones:
	 * 
	 * public void saveTo(HttpServletRequest request, HttpServletResponse response)
	 * public void removeFrom(HttpServletRequest request, HttpServletResponse response)
	 * public String readValue(HttpServletRequest request, HttpServletResponse ignored)
	 * 
	 * Sorry for long comments here and no comments on the code :)
	 * 
	 */

	public static final int DEFAULT_VERSION = -1;

    protected static final String NAME_VALUE_DELIMITER = "=";
    protected static final String ATTRIBUTE_DELIMITER = "; ";
    protected static final long DAY_MILLIS = 86400000; //1 day = 86,400,000 milliseconds
    protected static final String GMT_TIME_ZONE_ID = "GMT";
    protected static final String COOKIE_DATE_FORMAT_STRING = "EEE, dd-MMM-yyyy HH:mm:ss z";

    protected static final String COOKIE_HEADER_NAME = "Set-Cookie";
    protected static final String PATH_ATTRIBUTE_NAME = "Path";
    protected static final String EXPIRES_ATTRIBUTE_NAME = "Expires";
    protected static final String MAXAGE_ATTRIBUTE_NAME = "Max-Age";
    protected static final String DOMAIN_ATTRIBUTE_NAME = "Domain";
    protected static final String VERSION_ATTRIBUTE_NAME = "Version";
    protected static final String COMMENT_ATTRIBUTE_NAME = "Comment";
    protected static final String SECURE_ATTRIBUTE_NAME = "Secure";
    protected static final String HTTP_ONLY_ATTRIBUTE_NAME = "HttpOnly";

    private boolean httpOnly;

    public CookieEx(String name, String value) {
        super(name, value);
        this.httpOnly = false; // We are setting it false by default
    }

    public CookieEx(String name) {
    	this(name, "");
    }
    
    // Missing methods in Cookie
    public boolean getHttpOnly() {
        return httpOnly;
    }

    public void setHttpOnly(boolean httpOnly) {
        this.httpOnly = httpOnly;
    }

    private String calculatePath(HttpServletRequest request) {
    	
    	String path = clean(getPath());
        if (StringUtil.isEmpty(path)) {
            path = clean(request.getContextPath());
        }

        if (path == null) {
            path = "/";
        }

        return path;
    }

    private void addCookieHeader(HttpServletResponse response, String name, String value, String comment,
                                 String domain, String path, int maxAge, int version,
                                 boolean secure, boolean httpOnly) {

        String headerValue = buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
        response.addHeader(COOKIE_HEADER_NAME, headerValue);

    }

    /*
     * This implementation followed the grammar defined here for convenience:
     * <a href="http://github.com/abarth/http-state/blob/master/notes/2009-11-07-Yui-Naruse.txt">Cookie grammar</a>.
     *
     * @return the 'Set-Cookie' header value for this cookie instance.
     */

    private String buildHeaderValue(String name, String value, String comment,
                                      String domain, String path, int maxAge, int version,
                                      boolean secure, boolean httpOnly) {

        if (StringUtil.isEmpty(name)) {
            throw new IllegalStateException("Cookie name cannot be null/empty.");
        }

        StringBuilder sb = new StringBuilder(name).append(NAME_VALUE_DELIMITER);

        if (StringUtil.isNotEmpty(value)) {
            sb.append(value);
        }

        appendComment(sb, comment);
        appendDomain(sb, domain);
        appendPath(sb, path);
        appendExpires(sb, maxAge);
        appendVersion(sb, version);
        appendSecure(sb, secure);
        appendHttpOnly(sb, httpOnly);

        return sb.toString();

    }

    private void appendComment(StringBuilder sb, String comment) {
        if (StringUtil.isNotEmpty(comment)) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(COMMENT_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(comment);
        }
    }

    private void appendDomain(StringBuilder sb, String domain) {
        if (StringUtil.isNotEmpty(domain)) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(DOMAIN_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(domain);
        }
    }

    private void appendPath(StringBuilder sb, String path) {
        if (StringUtil.isNotEmpty(path)) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(PATH_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(path);
        }
    }

    private void appendExpires(StringBuilder sb, int maxAge) {
        // if maxAge is negative, cookie should should expire when browser closes
	// Don't write the maxAge cookie value if it's negative - at least on Firefox it'll cause the 
	// cookie to be deleted immediately
        // Write the expires header used by older browsers, but may be unnecessary
        // and it is not by the spec, see http://www.faqs.org/rfcs/rfc2965.html
        if (maxAge >= 0) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(MAXAGE_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(maxAge);
            sb.append(ATTRIBUTE_DELIMITER);
            Date expires;
            if (maxAge == 0) {
                //delete the cookie by specifying a time in the past (1 day ago):
                expires = new Date(System.currentTimeMillis() - DAY_MILLIS);
            } else {
                //Value is in seconds.  So take 'now' and add that many seconds, and that's our expiration date:
                Calendar cal = Calendar.getInstance();
                cal.add(Calendar.SECOND, maxAge);
                expires = cal.getTime();
            }
            String formatted = toCookieDate(expires);
            sb.append(EXPIRES_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(formatted);
        }
    }

    private void appendVersion(StringBuilder sb, int version) {
        if (version > DEFAULT_VERSION) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(VERSION_ATTRIBUTE_NAME).append(NAME_VALUE_DELIMITER).append(version);
        }
    }

    private void appendSecure(StringBuilder sb, boolean secure) {
        if (secure) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(SECURE_ATTRIBUTE_NAME); //No value for this attribute
        }
    }

    private void appendHttpOnly(StringBuilder sb, boolean httpOnly) {
        if (httpOnly) {
            sb.append(ATTRIBUTE_DELIMITER);
            sb.append(HTTP_ONLY_ATTRIBUTE_NAME); //No value for this attribute
        }
    }

    private static String toCookieDate(Date date) {
        TimeZone tz = TimeZone.getTimeZone(GMT_TIME_ZONE_ID);
        DateFormat fmt = new SimpleDateFormat(COOKIE_DATE_FORMAT_STRING, Locale.US);
        fmt.setTimeZone(tz);
        return fmt.format(date);
    }

    private static Cookie getCookie(HttpServletRequest request, String cookieName) {
        Cookie cookies[] = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(cookieName)) {
                    return cookie;
                }
            }
        }
        return null;
    }

    // Imported from org.apache.shiro.util.StringUtils    
    private static String clean(String in) {
        String out = in;

        if (in != null) {
            out = in.trim();
            if (out.equals("")) {
                out = null;
            }
        }

        return out;
    }

    public void saveTo(HttpServletRequest request, HttpServletResponse response) {
        String name = getName();
        String value = getValue();
        String comment = getComment();
        String domain = getDomain();
        String path = calculatePath(request);
        int maxAge = getMaxAge();
        int version = getVersion();
        boolean secure = getSecure();
        boolean httpOnly = getHttpOnly();

        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
    }

    // Important! You need to set the correct path before trying to remove it. 
    public void removeFrom(HttpServletRequest request, HttpServletResponse response) {

    	String name = getName();
        String value = "deleteMe";
        String comment = null; //don't need to add extra size to the response - comments are irrelevant for deletions
        String domain = getDomain();
        String path = calculatePath(request);
        int maxAge = 0; //always zero for deletion
        int version = getVersion();
        boolean secure = getSecure();
        boolean httpOnly = false; //no need to add the extra text, plus the value 'deleteMe' is not sensitive at all

        addCookieHeader(response, name, value, comment, domain, path, maxAge, version, secure, httpOnly);
    }

    public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
        String name = getName();
        String value = null;
        javax.servlet.http.Cookie cookie = getCookie(request, name);
        if (cookie != null) {
            value = cookie.getValue();
        }

        return value;
    }

    // XPages-specific methods
    private HttpServletRequest extractRequest() {
    	FacesContext context=FacesContext.getCurrentInstance();
		ExternalContext extContext=context.getExternalContext();

		return (HttpServletRequest) extContext.getRequest();
    }

    private HttpServletResponse extractResponse() {
    	FacesContext context=FacesContext.getCurrentInstance();
		ExternalContext extContext=context.getExternalContext();

		return (HttpServletResponse) extContext.getResponse();    	
    }

    public void save() {
    	saveTo(extractRequest(), extractResponse());
    }
    
    public void remove() {
    	removeFrom(extractRequest(), extractResponse());
    }
    
    public String readValue() {
    	return readValue(extractRequest(), extractResponse());
    }
    		
    public static void removeCookie(String name) {
    	removeCookie(name, "");
    }

    public static void removeCookie(String name, String path) {
    	CookieEx c=new CookieEx(name);
    	c.setPath(path);
    	c.remove();
    }
    // end of XPages specific methods
    
}





This snippet contains a Java class extending the standard Cookie of J2EE. The problem of current JRE we are using in XPages is that it doesn't support HttpOnly cookies. HttpOnly cookies are very important for security practices. In summary, if you mark a cookie as HttpOnly, browser will not let the client-side JavaScript code to access the value of that cookie. So it's a suggested method to protect the content of a cookie against possible client-side attack such as XSS or malicious browser plugins.

I have also seen many XPages developers having difficulties on removing cookies. To remove a cookie, you should add an expired cookie to the servlet response. This class contains an helper method to ease cookie removal.

The details on usage are included as a comment...

Java
Serdar Basegmez
December 9, 2013 5:20 PM
Rating
42

All code submitted to OpenNTF XSnippets, whether submitted as a "Snippet" or in the body of a Comment, is provided under the Apache License Version 2.0. See Terms of Use for full details.



No comments yetLogin first to comment...