Home > Java > Adding HttpOnly to RESTful response (Jee6)

Adding HttpOnly to RESTful response (Jee6)

Consider the following classes:

As you might see RESTful version lacks in HTTPOnly attribute (introduced with RFC 6265) because it only supports the first old RFC 2109. I don’t know which is the rational behind Jersey’s choice (latest version on RFC not implemented) but I need to solve the problem anyhow!

So, as “plain-old-jee-developer” :-D , the first solution I found was to write a servlet Filter and it worked great! Using new servlet 3.0 @WebFilter is not bad but…. googling around, I realized a more neat approach: injecting my own com.sun.jersey.spi.HeaderDelegateProvider implementation.

You only have to add a text file named

META-INF/services/com.sun.jersey.spi.HeaderDelegateProvider

containing the fully qualified name of your implementation. Your implementation must be  marked as Jersey @Provider.
UPDATE: Well, actually it’s not easy and working as it should be. I realized this tecnique does not work if jersey framework libraries are loaded before and in another classloader! So I had to change implementation with a sort of patch (tested on glassfish 3.1.2.2). I also fixed parser to be case insensitive.
Here my own implementation:

//@Provider will be substantially ignored because HeaderDelegateProvider is such an internal "special"
//The only way to look up this implementation is by using standard OSGi approach directly searching via ServiceFinder.find()
//As an example, look at NewCookie2 class
public class NewCookie2HeaderDelegateProvider implements HeaderDelegateProvider<NewCookie2> {

  private static final Logger log = Logger.getLogger(NewCookie2HeaderDelegateProvider.class.getName());

  public NewCookie2HeaderDelegateProvider(){
    log.info("NewCookie2HeaderDelegateProvider() called.");
  }

  @Override
  public NewCookie2 fromString(String header) throws IllegalArgumentException {
    if (header == null) {
      throw new IllegalArgumentException("header must not be null!");
    }
    return NewCookie2Parser.parseNewCookie2(header);
  }

  @Override
  public String toString(NewCookie2 cookie) {
    StringBuilder b = new StringBuilder();

    b.append(cookie.getName()).append('=');
    WriterUtil.appendQuotedIfWhitespace(b, cookie.getValue());

    b.append(";").append(" Version=").append(cookie.getVersion());

    if (cookie.getComment() != null) {
      b.append("; Comment=");
      WriterUtil.appendQuotedIfWhitespace(b, cookie.getComment());
    }
    if (cookie.getDomain() != null) {
      b.append("; Domain=");
      WriterUtil.appendQuotedIfWhitespace(b, cookie.getDomain());
    }
    if (cookie.getPath() != null) {
      b.append("; Path=");
      WriterUtil.appendQuotedIfWhitespace(b, cookie.getPath());
    }
    if (cookie.getMaxAge()!=-1) {
      b.append("; Max-Age=");
      b.append(cookie.getMaxAge());
    }
    if (cookie.isSecure()) {
      b.append("; Secure");
    }

    if (cookie.isHttpOnly()){
      b.append("; HTTPOnly");
    }

    return b.toString();        
  }

  @Override
  public boolean supports(Class<?> type) {
    return type == NewCookie2.class;
  }
}

where my “NewCookie2” extends Jersey “NewCookie”:

@SuppressWarnings("unchecked")
public class NewCookie2 extends NewCookie {
  private static final Logger log = Logger.getLogger(NewCookie2.class.getName());
  //Provider will be searched using ServiceFinder.find() in current thread's classloader which means, our jee module.
  private static HeaderDelegateProvider<NewCookie2> provider;
  @SuppressWarnings("rawtypes")
  static ServiceFinder<HeaderDelegateProvider> providerFinder = ServiceFinder.find(HeaderDelegateProvider.class,Thread.currentThread().getContextClassLoader(), false);
  static{
    for(@SuppressWarnings("rawtypes") HeaderDelegateProvider p : providerFinder){
      log.info("searching HeaderDelegateProviders..");
      if (p.supports(NewCookie2.class)){
        provider = p;
        log.info("HeaderDelegateProvider for NewCookie2 found: "+provider.getClass().getName());
        break;
      }
    }
    if (provider==null){
      throw new IllegalStateException("There's no HeaderDelegateProvider implementation supporting NewCookie2. Please be sure META-INF/services contains right file!");
    }
  }

  private boolean httpOnly = false;

  public NewCookie2(Cookie cookie) {
    super(cookie);
  }

  public NewCookie2(Cookie cookie, boolean httpOnly) {
    super(cookie);
    this.httpOnly = httpOnly;
  }
......

   public NewCookie2(String name, String value, String path, String domain,
      int version, String comment, int maxAge, boolean secure, boolean httpOnly) {
    super(name, value, path, domain, version, comment, maxAge, secure);
    this.httpOnly = httpOnly;
  }

  public boolean isHttpOnly() {
    return httpOnly;
  }

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

  /**
   * Creates a new instance of NewCookie2 by parsing the supplied string.
   * @param value the cookie string
   * @return the newly created NewCookie2
   * @throws IllegalArgumentException if the supplied string cannot be parsed
   * or is null
   */
  public static NewCookie2 valueOf(String value) throws IllegalArgumentException {
    return provider.fromString(value);
  }

  /**
   * Convert the cookie to a string suitable for use as the value of the
   * corresponding HTTP header.
   * @return a stringified cookie
   */
  @Override
  public String toString() {
    return provider.toString(this);
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = super.hashCode();
    result = prime * result + (httpOnly ? 1231 : 1237);
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!super.equals(obj)) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    NewCookie2 other = (NewCookie2) obj;
    if (httpOnly != other.httpOnly) {
      return false;
    }
    return true;
  }
}

and a new parser takes care ok HTTPOnly attribute also:

public class NewCookie2Parser {

private static class MutableNewCookie2 {
String name = null;
String value = null;
String path = null;
String domain = null;
int version = Cookie.DEFAULT_VERSION;
String comment = null;
int maxAge = NewCookie.DEFAULT_MAX_AGE;
boolean secure = false;
boolean httpOnly = false;

public MutableNewCookie2(String name, String value) {
this.name = name;
this.value = value;
}

public NewCookie2 getImmutableNewCookie2() {
return new NewCookie2(name, value, path, domain, version, comment, maxAge, secure, httpOnly);
}
}

public class NewCookie2Parser {

  private static class MutableNewCookie2 {
    String name = null;
    String value = null;
    String path = null;
    String domain = null;
    int version = Cookie.DEFAULT_VERSION;
    String comment = null;
    int maxAge = NewCookie.DEFAULT_MAX_AGE;
    boolean secure = false;
    boolean httpOnly = false;

    public MutableNewCookie2(String name, String value) {
      this.name = name;
      this.value = value;
    }

    public NewCookie2 getImmutableNewCookie2() {
      return new NewCookie2(name, value, path, domain, version, comment, maxAge, secure, httpOnly);
    }        
  }

  public static NewCookie2 parseNewCookie2(String header) {
    String bites[] = header.split("[;,]");

    MutableNewCookie2 cookie = null;
    for (String bite: bites) {
      String crumbs[] = bite.split("=", 2);
      String name = crumbs.length>0 ? crumbs[0].trim() : "";
      String value = crumbs.length>1 ? crumbs[1].trim() : "";
      if (value.startsWith("\"") && value.endsWith("\"") && value.length()>1) {
        value = value.substring(1,value.length()-1);
      }

      if (cookie == null) {
        cookie = new MutableNewCookie2(name, value);
      } else if (name.toLowerCase().startsWith("comment")) {
        cookie.comment = value;
      } else if (name.toLowerCase().startsWith("domain")) {
        cookie.domain = value;
      } else if (name.startsWith("max-age")) {
        cookie.maxAge = Integer.parseInt(value);
      } else if (name.toLowerCase().startsWith("path")) {
        cookie.path = value;
      } else if (name.toLowerCase().startsWith("secure")) {
        cookie.secure = true;
      } else if (name.toLowerCase().startsWith("version")) {
        cookie.version = Integer.parseInt(value);
      } else if (name.toLowerCase().startsWith("domain")) {
        cookie.domain = value;
      } else if (name.toLowerCase().startsWith("httponly")) {
        cookie.httpOnly = true;
      }
    }

    return cookie.getImmutableNewCookie2();
  }
}

Obviously this is not meant to be a complete RFC 6265 implementation but it fits my needs.
As an example of a usage inside your own jersey resource, here is a snippet:

@POST
public javax.ws.rs.core.Response loginRedirect(@FormParam("user") String userName,
@FormParam("password") String password){
Response.ResponseBuilder responseBuilder = null;

//check username and password and if correct:

responseBuilder = addCookie(Response.created(uriOK).status(Status.MOVED_PERMANENTLY));

return responseBuilder.build();

}

where addCookie is:

protected static Response.ResponseBuilder addCookie(Response.ResponseBuilder responseToAddTo){
Cookie authCookie = new Cookie("AUTH", "134142134", "/", ".mydomain.it");
NewCookie2 newCookie = new NewCookie2(authCookie, true);
responseToAddTo.cookie(newCookie);
CacheControl cc=new CacheControl();
cc.setMaxAge(7200);
responseToAddTo.cacheControl(cc);
return responseToAddTo;
}

Hope this helps.
Let me know!

Categorie:Java Tag:, , , ,
  1. curlup
    giugno 27, 2012 alle 09:29

    hi! havent you supposed to create a patch for jersey?
    new jersey2 also lack of httpOnly feature for NewCookie

    • giugno 29, 2012 alle 08:12

      you’re right but I have not time by now :-| If you are planning to do it, please insert this post in your code ;-)

  2. settembre 13, 2012 alle 12:00

    i’d love to see this become part of the NewCookie class – it’s a basic security issue.

  3. settembre 13, 2012 alle 17:42

    hi! i tried your approach, but i can’t get it to work. I’v put some debug points in ALL of the methods of the new classes you introduced, but they never get hit – as if the provider is not registered properly, yet when starting the app it says:

    “INFO: Provider classes found:
    class com.test.NewCookie2HeaderDelegateProvider.”

    I’ve been able to get it to show both by including a txt file like you suggested as well as programatically:

    resourceConfig.getClasses().add(com.test.NewCookie2HeaderDelegateProvider.class);.

    I’m playing around with jersey version: 1.13-b01.

    No luck! Let me know if you have any ideas on what might be wrong, or if you can suggest a way to further investigate the issue. Btw thanks for the article!

    • settembre 13, 2012 alle 20:09

      hi! that’s strange. I was able to get it working within glassfish 3.1.2 which uses jersey 1.11. However, if you’ve bee able to see message “INFO: Provider classes found: class com.test.NewCookie2HeaderDelegateProvider” means jersey recognizes your class and will delegate process to it.
      What exactly is not working? Doesn’t add “;HttpOnly” on your response header?

  4. settembre 14, 2012 alle 17:24

    Thanks for the reply! Yes really strange – in the log it says it has found it as a provider, but it never kicks in. I’ve put breakpoints all over the methods, constructors of the above described 3 classes hoping it will stop in debug mode, but it never does. It’s loaded, but not used. Strange. Maybe i am doing something wrong. When i send a cookie back as a Response.cookie it just arrives as a regular NewCookie (the NewCookie2 constructor breakpoint kicks in, but not with the new writer). For example i have a breakpoint in here, but it does not get hit:
    public NewCookie2HeaderDelegateProvider(){
    super();
    newCookieProvider = new NewCookieProvider();
    }

    • gennaio 15, 2013 alle 10:47

      hi vedranstanic,
      sorry for the delay and maybe you gave up this solution (or you found another way). Maybe I found the problem you had. I’ve updated my code with a patch and some fix (see above). Another very simple and fast solution is the one mfornos suggests.
      Reguards.
      Vins

  5. gennaio 13, 2013 alle 21:04

    Hi, another solution is implementing a class like that:

    
    import javax.ws.rs.core.NewCookie;
    
    public class SecureCookie extends NewCookie {
    
        private final boolean httpOnly;
    
        public SecureCookie(String name, String value, String path, String domain, int version, String comment, int maxAge, boolean secure, boolean httpOnly) {
    
            super(name, value, path, domain, version, comment, maxAge, secure);
            this.httpOnly = httpOnly;
        
       }
    
        public SecureCookie(String name, String value, boolean httpOnly) {
            this(name, value, "/", null, 1, null, -1, false, httpOnly);
        }
    
        public String toString() {
            return httpOnly ? new StringBuilder(super.toString()).append(";HTTPOnly").toString() : super.toString();
        }
    
    }
    

    And use it:

    
    
    Response.ok().cookie(new SecureCookie("key", "value", true));
    
    

    And that’s all, no requires custom provider etc. (tested with jersey 1.16)

    cheers ;)

    • gennaio 14, 2013 alle 14:27

      Hi mfornos,
      implementing (extending) the NewCookie only allows you to customize the serialization of your own cookie and not its deserialization. Custom provider will perform that under the hood. The aim of the article was to find a valid and elegant solution for both problems ( the same way Jersey does ) but if you only need to produce a special cookie, your solution is obviosly faster.
      Anyway, I found the reason why many people had troubles looking up providers. I’ll update my article as soon as I can.

      • gennaio 14, 2013 alle 20:33

        Nope, also works for deserialization, because the default HeaderProvider uses the toString method for deserialization (wich uses the NewCookieProvier under the hood), and as you see it’s overridden to add the HttpOnly parameter.

        ... toString() {
          return httpOnly ? 
            new StringBuilder(super.toString()).append(";HTTPOnly") 
            : super.toString();
        }
        

        Try it. :)

  6. gennaio 15, 2013 alle 10:43

    mfornos,
    http://en.wikipedia.org/wiki/Serialization will be performed by jersey which indirectly calls .toString() when you are building response but the approach you suggest only works for serialization phase and only if jersey-core classloader “sees” your own class. Did you package jersey-core.jar (and all related jars) into WEB-INF/lib ? Well, in that case serialization surely works, but if you remove jersey from your war and put all jersey frameworks stuff into your application server lib directory, it won’t work. I had no time to test latest jersey releases but I had a rapid look at changelogs ( http://java.net/projects/jersey/sources/svn/content/tags/jersey-1.16/jersey/changes.txt ) and I couldn’t find anything about this argument.
    Deserialization will be performed when jersey receives headers and creates your own object. Actually “httponly” won’t be sent by the client (because it’s a parameter that server will send to it but it’s useless for server itself). I added it in deserializaion method .fromString() just in case.
    The overall usage of delegate class is just a way to have a more structured way (proposed by jersey itself) to fase future possible customizations.

    Cheers.

    • gennaio 15, 2013 alle 11:05

      Yes, you’re right. Sorry for the misleading point about deserialization, my fault. :)

  7. gennaio 15, 2013 alle 11:38

    However, deserializing the HttpOnly attribute on the server side makes sense? I suppose that you do this to update the cookie state, but it can be easily achieved wrapping again the cookie in your custom one. So, for the most of use cases that now I can imagine (sure I’m leaving some) automatic deserialization is not required.

    cheers and thanks.

  1. settembre 7, 2014 alle 00:46

Lascia un commento

Effettua il login con uno di questi metodi per inviare il tuo commento:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...

%d blogger cliccano Mi Piace per questo: