You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/11/16 18:29:14 UTC

[4/6] brooklyn-server git commit: switch CSRF to use cookies for tokens

switch CSRF to use cookies for tokens

now supports AngularJS semantics.
also now it doesn't needlessly create sessions.


Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo
Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/c8186f8d
Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/c8186f8d
Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/c8186f8d

Branch: refs/heads/master
Commit: c8186f8df25a72caf885020b2536a94ae976df23
Parents: ce0db93
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Sun Nov 13 02:45:58 2016 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Sun Nov 13 09:39:52 2016 -0700

----------------------------------------------------------------------
 .../brooklyn/rest/filter/CsrfTokenFilter.java   | 272 +++++++++++++++----
 .../rest/CsrfTokenFilterLauncherTest.java       |  54 ++--
 .../brooklyn/util/http/HttpToolResponse.java    |  26 ++
 3 files changed, 275 insertions(+), 77 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
----------------------------------------------------------------------
diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
index aa7e62a..67889bd 100644
--- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
+++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CsrfTokenFilter.java
@@ -19,69 +19,191 @@
 package org.apache.brooklyn.rest.filter;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
 
 import javax.annotation.Priority;
 import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.HttpMethod;
+import javax.servlet.http.HttpSession;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ContainerRequestFilter;
 import javax.ws.rs.container.ContainerResponseContext;
 import javax.ws.rs.container.ContainerResponseFilter;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.NewCookie;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.Provider;
 
+import org.apache.brooklyn.core.mgmt.entitlement.Entitlements;
+import org.apache.brooklyn.rest.api.ServerApi;
 import org.apache.brooklyn.rest.domain.ApiError;
 import org.apache.brooklyn.util.text.Identifiers;
+import org.apache.brooklyn.util.text.Strings;
+import org.apache.commons.collections.EnumerationUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 
 /**
- * If client specifies {@link #REQUEST_CSRF_TOKEN_HEADER}
- * (as any of the values {@link #REQUEST_CSRF_TOKEN_HEADER_LOGIN}, {@link #REQUEST_CSRF_TOKEN_HEADER_NEW}, {@link #REQUEST_CSRF_TOKEN_HEADER_TRUE})
- * then the server will return a {@link #REQUIRED_CSRF_TOKEN_HEADER} header containing a token.
+ * Causes the server to return a "Csrf-Token" cookie in certain circumstances,
+ * and thereafter to require clients to send that token as an "X-Csrf-Token" header on certain requests.
+ * <p>
+ * To enable REST requests to work from the <code>br</code> CLI and regular <code>curl</code> without CSRF,
+ * this CSRF protection only applies when a session is in effect.
+ * Sessions are only established by some REST requests:
+ * {@link ServerApi#getUpExtended()} is a good choice (/v1/server/up/extended).
+ * It can also be forced by using the {@link #CSRF_TOKEN_REQUIRED_HEADER} header on any REST request.
+ * Browser-based UI clients should use one of the above methods early in operation.
+ * <p>
+ * The standard model is that the token is required for all <i>write</i> (ie non-GET) requests being made
+ * with a valid session cookie (ie the request is part of an ongoing session).
+ * In such cases, the client must send the X-Csrf-Token as a header.
+ * This prevents a third-party site from effecting a mutating cross-site request via the browser. 
+ * <p>
+ * For transitional reasons, the default behaviour in the current implementation is to warn (not fail)
+ * if no token is supplied in the above case. This will likely be changed to enforce the standard model 
+ * in a subsequent version, but it avoids breaking backwards compatibility for any existing session-based clients.
+ * <p>
+ * Clients can force different required behaviour (e.g. "fail") by including the
+ * {@link #CSRF_TOKEN_REQUIRED_HEADER} with one of the values in {@link CsrfTokenRequiredForRequests},
+ * viz. to require the header on ALL requests, or on NONE, instead of just on WRITE requests (the default).
+ * If such a mode is explicitly specified it is enforced (instead of just displaying the transitional warning),
+ * so while transitioning to enforce CSRF this header should be supplied.
+ * The Brooklyn UI does this.
+ * <p>
+ * This uses *two* names, <code>Csrf-Token</code> and <code>Xsrf-Token</code>.
+ * The former seems the usual convention, but Angular apps use the latter.
+ * This strategy should mean that Angular apps should get CSRF protection with no special configuration.
+ * <p>
+ * The cookies are sent by the client on every request, by virtue of being cookies,
+ * although this is not necessary (and rather wasteful). A client may optimise by deleting the cookies 
+ * and caching the information in another form. 
+ * The cookie names do not have any salt/uid, so in dev on localhost you may get conflicts, e.g. between
+ * multiple Brooklyn instances or other apps that use the same token names.
+ * (In contrast the session ID token, usually JSESSIONID, has a BROOKLYN<uid> field at runtime to prevent
+ * such collisions.)
  * <p>
- * The server will require that header with the given token on subsequent POST requests
- * (and other actions -- excluding GET which is always permitted;
- * future enhancement may allow client to control whether GET is allowed or not).
+ * Additional CSRF strategies might be worth considering, in addition to the one described above:
+ * <ul>
+ * <li> Compare "Referer / Origin" against "Host" / "X-Forwarded-Host" (or app knowing its location)
+ *      (disallowing if different is probably a good idea)
+ * <li> Look for "X-Requested-With: XMLHttpRequest" on POST, esp if there is cookie and/or user agent is a browser
+ *      (but angular drops this one)
+ * </ul>
+ * More info at:  https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet .
+ * (These have not been implemented because the cookie pattern is sufficient, and one of the strongest.)
+ * 
  */
 @Provider
 @Priority(400)
 public class CsrfTokenFilter implements ContainerRequestFilter, ContainerResponseFilter {
     
-    public static final String REQUIRED_CSRF_TOKEN_ATTR = CsrfTokenFilter.class.getName()+"."+"REQUIRED_CSRF_TOKEN";
+    private static final Logger log = LoggerFactory.getLogger(CsrfTokenFilter.class);
+
+    private static final String CSRF_TOKEN_VALUE_BASE = "Csrf-Token";
+    public static final String CSRF_TOKEN_VALUE_COOKIE = CSRF_TOKEN_VALUE_BASE.toUpperCase();
+    public static final String CSRF_TOKEN_VALUE_HEADER = HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_BASE);
+    // also send this so angular works as expected
+    public static final String CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME = "Xsrf-Token";
+    public static final String CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME = CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME.toUpperCase();
+    public static final String CSRF_TOKEN_VALUE_HEADER_ANGULAR_NAME = HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_BASE_ANGULAR_NAME);
+    
+    public static final String CSRF_TOKEN_VALUE_ATTR = CsrfTokenFilter.class.getName()+"."+CSRF_TOKEN_VALUE_COOKIE;
     
-    public static final String REQUIRED_CSRF_TOKEN_HEADER = CsrfTokenFilter.class.getName()+"."+"X-CsrfToken";
-    public static final String REQUEST_CSRF_TOKEN_HEADER = CsrfTokenFilter.class.getName()+"."+"X-CsrfToken-Request";
-    /** means to return the token */
-    public static final String REQUEST_CSRF_TOKEN_HEADER_TRUE = "true";
-    /** means to create a new token */
-    public static final String REQUEST_CSRF_TOKEN_HEADER_NEW = "new";
-    /** means to create and return a token on login only (ie if one doesn't exist for the session) */
-    public static final String REQUEST_CSRF_TOKEN_HEADER_LOGIN = "login";
+    public static String HEADER_OF_COOKIE(String cookieName) {
+        return "X-"+cookieName;
+    }
     
+    public static final String CSRF_TOKEN_REQUIRED_HEADER = "X-Csrf-Token-Required-For-Requests";
+    public static final String CSRF_TOKEN_REQUIRED_ATTR = CsrfTokenFilter.class.getName()+"."+CSRF_TOKEN_REQUIRED_HEADER;
+    public static enum CsrfTokenRequiredForRequests { ALL, WRITE, NONE, }
+    public static CsrfTokenRequiredForRequests DEFAULT_REQUIRED_FOR_REQUESTS = CsrfTokenRequiredForRequests.WRITE;
+
     @Context
     private HttpServletRequest request;
 
     @Override
     public void filter(ContainerRequestContext requestContext) throws IOException {
-        Object requiredToken = request.getSession().getAttribute(REQUIRED_CSRF_TOKEN_ATTR);
-        if (requiredToken!=null) {
-            String suppliedToken = request.getHeader(REQUIRED_CSRF_TOKEN_HEADER);
-            if (suppliedToken==null) {
-                if (HttpMethod.GET.equals(requestContext.getMethod())) {
-                    // GETs are permitted with no token (but an invalid token will be caught)
-                    return;
-                }
-
-                fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
-                        .message(REQUIRED_CSRF_TOKEN_HEADER+" header required containing token previously supplied")
-                        .build());
-            } else if (suppliedToken.equals(requiredToken)) {
-                // approved
+        // if header supplied, check it is valid
+        String requiredWhenS = request.getHeader(CSRF_TOKEN_REQUIRED_HEADER);
+        if (Strings.isNonBlank(requiredWhenS) && getRequiredForRequests(requiredWhenS, null)==null) {
+            fail(requestContext, ApiError.builder().errorCode(Response.Status.BAD_REQUEST)
+                .message(HEADER_OF_COOKIE(CSRF_TOKEN_REQUIRED_HEADER)+" header if supplied must be one of "
+                    + Arrays.asList(CsrfTokenRequiredForRequests.values()))
+                .build());
+            return;
+    }
+        
+        if (!request.isRequestedSessionIdValid()) {
+            // no session; just return
+            return;
+        }
+        
+        @SuppressWarnings("unchecked")
+        List<String> suppliedTokensDefault = (List<String>) EnumerationUtils.toList(request.getHeaders(CSRF_TOKEN_VALUE_HEADER));
+        @SuppressWarnings("unchecked")
+        List<String> suppliedTokensAngular = (List<String>) EnumerationUtils.toList(request.getHeaders(CSRF_TOKEN_VALUE_HEADER_ANGULAR_NAME));
+        List<String> suppliedTokens = Lists.newArrayList(suppliedTokensDefault);
+        suppliedTokens.addAll(suppliedTokensAngular);
+
+        Object requiredToken = request.getSession().getAttribute(CSRF_TOKEN_VALUE_ATTR);
+        CsrfTokenRequiredForRequests whenRequired = (CsrfTokenRequiredForRequests) request.getSession().getAttribute(CSRF_TOKEN_REQUIRED_ATTR);
+
+        boolean isRequired;
+        
+        if (whenRequired==null) {
+            if (suppliedTokens.isEmpty()) {
+                log.warn("No CSRF token expected or supplied but a cookie-session is active: client should be updated"
+                    + " (in future CSRF tokens or instructions may be required for session-based clients)"
+                    + " - " + Entitlements.getEntitlementContext());
+                whenRequired = CsrfTokenRequiredForRequests.NONE;
             } else {
-                fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
-                    .message(REQUIRED_CSRF_TOKEN_HEADER+" header did not match current token")
-                    .build());
+                // default
+                whenRequired = DEFAULT_REQUIRED_FOR_REQUESTS;
             }
+            // remember it to suppress warnings subsequently
+            request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, whenRequired);
+        }
+        
+        switch (whenRequired) {
+        case NONE:
+            isRequired = false;
+            break;
+        case WRITE:
+            isRequired = !org.eclipse.jetty.http.HttpMethod.GET.toString().equals(requestContext.getMethod());
+            break;
+        case ALL:
+            isRequired = true;
+            break;
+        default:
+            log.warn("Unexpected "+CSRF_TOKEN_REQUIRED_ATTR+" value "+whenRequired);
+            isRequired = true;
+        }
+        
+        if (Iterables.any(suppliedTokens, Predicates.equalTo(requiredToken))) {
+            // matching token supplied, or not being used 
+            return;
+        }
+        
+        if (!isRequired) {
+            // csrf not required, but it doesn't match
+            if (requiredToken!=null) {
+                log.trace("CSRF optional token mismatch: client did not send valid token, but it isn't required so proceeding");
+            }
+            return;
+        }
+
+        if (suppliedTokens.isEmpty()) {
+            fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
+                    .message(HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_COOKIE)+" header is required, containing token previously returned from server in cookie")
+                    .build());
+        } else {
+            fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
+                .message(HEADER_OF_COOKIE(CSRF_TOKEN_VALUE_COOKIE)+" header did not match expected CSRF token")
+                .build());
         }
     }
 
@@ -91,37 +213,69 @@ public class CsrfTokenFilter implements ContainerRequestFilter, ContainerRespons
 
     @Override
     public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
-        String requestHeader = request.getHeader(REQUEST_CSRF_TOKEN_HEADER);
-        if (requestHeader==null) return;
-        String oldToken = (String) request.getSession().getAttribute(REQUIRED_CSRF_TOKEN_ATTR);
-        String tokenToReturn = null;
-        boolean createToken = false;
+        HttpSession session = request.getSession(false);
+        String token = (String) (session==null ? null : session.getAttribute(CSRF_TOKEN_VALUE_ATTR));
+        String requiredWhenS = request.getHeader(CSRF_TOKEN_REQUIRED_HEADER);
         
-        if (REQUEST_CSRF_TOKEN_HEADER_LOGIN.equals(requestHeader)) {
-            if (oldToken==null) createToken = true;
-            else return;
-        } else if (REQUEST_CSRF_TOKEN_HEADER_TRUE.equals(requestHeader)) {
-            tokenToReturn = oldToken;
-            if (tokenToReturn==null) createToken = true;
-        } else if (REQUEST_CSRF_TOKEN_HEADER_NEW.equals(requestHeader)) {
-            createToken = true;
-        }
-    
-        if (createToken) {
-            tokenToReturn = Identifiers.makeRandomId(16);
-            request.getSession().setAttribute(REQUIRED_CSRF_TOKEN_ATTR, tokenToReturn);
+        if (session==null) {
+            if (Strings.isBlank(requiredWhenS)) {
+                // no session and no requirement specified, bail out 
+                return;
+            }
+            // explicit requirement request forces a session  
+            session = request.getSession();
         }
         
-        if (tokenToReturn==null) {
-            fail(requestContext, ApiError.builder().errorCode(Response.Status.UNAUTHORIZED)
-                .message(REQUEST_CSRF_TOKEN_HEADER+" contained invalid value; expected: "+
-                    REQUEST_CSRF_TOKEN_HEADER_TRUE+" | "+
-                    REQUEST_CSRF_TOKEN_HEADER_LOGIN+" | "+
-                    REQUEST_CSRF_TOKEN_HEADER_NEW)
-                .build());
+        CsrfTokenRequiredForRequests requiredWhen;
+        if (Strings.isNonBlank(requiredWhenS)) {
+            requiredWhen = getRequiredForRequests(requiredWhenS, DEFAULT_REQUIRED_FOR_REQUESTS);
+            request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, requiredWhen);
+            if (Strings.isNonBlank(token)) {
+                // already set csrf token, and the client got it
+                // (with the session token if they are in a session;
+                // or if client didn't get it it isn't in a session)
+                return;
+            }
         } else {
-            responseContext.getHeaders().add(REQUIRED_CSRF_TOKEN_HEADER, tokenToReturn);
+            if (Strings.isNonBlank(token)) {
+                // already set csrf token, and the client got it
+                // (with the session token if they are in a session;
+                // or if client didn't get it it isn't in a session)
+                return;
+            }
+            requiredWhen = (CsrfTokenRequiredForRequests) request.getSession().getAttribute(CSRF_TOKEN_REQUIRED_ATTR);
+            if (requiredWhen==null) {
+                requiredWhen = DEFAULT_REQUIRED_FOR_REQUESTS;
+                request.getSession().setAttribute(CSRF_TOKEN_REQUIRED_ATTR, requiredWhen);
+            }
+        }
+
+        if (requiredWhen==CsrfTokenRequiredForRequests.NONE) {
+            // not required; so don't set
+            return;
+        }
+
+        // create the token
+        token = Identifiers.makeRandomId(16);
+        request.getSession().setAttribute(CSRF_TOKEN_VALUE_ATTR, token);
+        
+        addCookie(responseContext, CSRF_TOKEN_VALUE_COOKIE, token, "Clients should send this value in header "+CSRF_TOKEN_VALUE_HEADER+" for validation");
+        addCookie(responseContext, CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME, token, "Compatibility cookie for "+CSRF_TOKEN_VALUE_COOKIE+" following AngularJS conventions");
+    }
+
+    protected NewCookie addCookie(ContainerResponseContext responseContext, String cookieName, String token, String comment) {
+        NewCookie cookie = new NewCookie(cookieName, token, "/", null, comment, -1, false);
+        responseContext.getHeaders().add("Set-Cookie", cookie);
+        return cookie;
+    }
+
+    protected CsrfTokenRequiredForRequests getRequiredForRequests(String requiredWhenS, CsrfTokenRequiredForRequests defaultMode) {
+        CsrfTokenRequiredForRequests result = null;
+        if (requiredWhenS!=null) {
+            result = CsrfTokenRequiredForRequests.valueOf(requiredWhenS.toUpperCase());
         }
+        if (result!=null) return result;
+        return defaultMode;
     }
 
 }

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
index f9f2ec2..a9af873 100644
--- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
+++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java
@@ -23,6 +23,7 @@ import static org.testng.Assert.assertEquals;
 
 import java.net.URI;
 import java.util.List;
+import java.util.Map;
 
 import javax.ws.rs.core.HttpHeaders;
 
@@ -48,49 +49,66 @@ public class CsrfTokenFilterLauncherTest extends BrooklynRestApiLauncherTestFixt
             .withoutJsgui()
             .start());
 
+        HttpClient client = client();
+        
         HttpToolResponse response = HttpTool.httpGet(
-            client(), URI.create(getBaseUriRest() + "server/status"),
+            client, URI.create(getBaseUriRest() + "server/status"),
             ImmutableMap.<String,String>of(
-                CsrfTokenFilter.REQUEST_CSRF_TOKEN_HEADER, CsrfTokenFilter.REQUEST_CSRF_TOKEN_HEADER_TRUE ));
+                CsrfTokenFilter.CSRF_TOKEN_REQUIRED_HEADER, CsrfTokenFilter.CsrfTokenRequiredForRequests.WRITE.toString()));
         
         // comes back okay
         assertOkayResponse(response, "MASTER");
         
-        System.out.println(response.getHeaderLists());
-        List<String> tokens = response.getHeaderLists().get(CsrfTokenFilter.REQUIRED_CSRF_TOKEN_HEADER);
-        String token = Iterables.getOnlyElement(tokens);
+        Map<String, List<String>> cookies = response.getCookieKeyValues();
+        String token = Iterables.getOnlyElement(cookies.get(CsrfTokenFilter.CSRF_TOKEN_VALUE_COOKIE));
         Assert.assertNotNull(token);
+        String tokenAngular = Iterables.getOnlyElement(cookies.get(CsrfTokenFilter.CSRF_TOKEN_VALUE_COOKIE_ANGULAR_NAME));
+        Assert.assertEquals(token, tokenAngular);
         
-        List<String> cookies = response.getHeaderLists().get(HttpHeaders.SET_COOKIE);
-        String cookie = Iterables.getOnlyElement(cookies);
-        Assert.assertNotNull(cookie);
-
         // can post subsequently with token
         response = HttpTool.httpPost(
-            client(), URI.create(getBaseUriRest() + "script/groovy"),
+            client, URI.create(getBaseUriRest() + "script/groovy"),
             ImmutableMap.<String,String>of(
                 HttpHeaders.CONTENT_TYPE, "application/text",
-                HttpHeaders.COOKIE, cookie,
-                CsrfTokenFilter.REQUIRED_CSRF_TOKEN_HEADER, token ),
+                CsrfTokenFilter.CSRF_TOKEN_VALUE_HEADER, token ),
             "return 0;".getBytes());
         assertOkayResponse(response, "{\"result\":\"0\"}");
 
         // but fails without token
         response = HttpTool.httpPost(
-            client(), URI.create(getBaseUriRest() + "script/groovy"),
+            client, URI.create(getBaseUriRest() + "script/groovy"),
             ImmutableMap.<String,String>of(
-                HttpHeaders.COOKIE, cookie,
                 HttpHeaders.CONTENT_TYPE, "application/text" ),
             "return 0;".getBytes());
         assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
 
-        // but you can get subsequently without token
+        // can get without token
         response = HttpTool.httpGet(
-            client(), URI.create(getBaseUriRest() + "server/status"),
+            client, URI.create(getBaseUriRest() + "server/status"),
+            ImmutableMap.<String,String>of());
+        assertOkayResponse(response, "MASTER");
+        
+        // but if we set required ALL then need a token to get
+        response = HttpTool.httpGet(
+            client, URI.create(getBaseUriRest() + "server/status"),
             ImmutableMap.<String,String>of(
-                HttpHeaders.COOKIE, cookie ));
-
+                CsrfTokenFilter.CSRF_TOKEN_REQUIRED_HEADER, CsrfTokenFilter.CsrfTokenRequiredForRequests.ALL.toString().toLowerCase()
+                ));
         assertOkayResponse(response, "MASTER");
+        response = HttpTool.httpGet(
+            client, URI.create(getBaseUriRest() + "server/status"),
+            ImmutableMap.<String,String>of());
+        assertEquals(response.getResponseCode(), HttpStatus.SC_UNAUTHORIZED);
+        
+        // however note if we use a new client, with no session, then we can post with no token
+        // (ie we don't guard against CSRF if your brooklyn is unsecured)
+        client = client();
+        response = HttpTool.httpPost(
+            client, URI.create(getBaseUriRest() + "script/groovy"),
+            ImmutableMap.<String,String>of(
+                HttpHeaders.CONTENT_TYPE, "application/text" ),
+            "return 0;".getBytes());
+        assertOkayResponse(response, "{\"result\":\"0\"}");
     }
 
     protected HttpClient client() {

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/c8186f8d/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java b/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
index 811b3e1..27dc237 100644
--- a/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
+++ b/utils/common/src/main/java/org/apache/brooklyn/util/http/HttpToolResponse.java
@@ -39,8 +39,10 @@ import org.slf4j.LoggerFactory;
 import com.google.common.base.Objects;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.io.ByteStreams;
+import com.google.common.net.HttpHeaders;
 
 /**
  * A response class for use with {@link HttpTool}.
@@ -149,6 +151,30 @@ public class HttpToolResponse {
         return headerLists;
     }
     
+    public Map<String, List<String>> getCookieKeyValues() {
+        List<String> cookiesRaw = getHeaderLists().get(HttpHeaders.SET_COOKIE);
+        Map<String, List<String>> result = Maps.newLinkedHashMap();
+        for (String c: cookiesRaw) {
+            // poor man's parse; would need to copy routines in jetty's CookieCutter (not in scope) or similar
+            // to treat quotes/escapes correctly, and ideally return typed cookies
+            
+            int keyI = c.indexOf('=');
+            if (keyI<0) continue; //not a valid cookie
+            String key = c.substring(0,  keyI);
+            String value = c.substring(keyI+1);
+            int flagsI = value.indexOf(';');
+            if (flagsI>=0) value = value.substring(0, flagsI);
+            
+            List<String> values = result.get(key);
+            if (values==null) {
+                values = Lists.newArrayList();
+                result.put(key, values);
+            }
+            values.add(value);
+        }
+        return result;
+    }
+    
     public byte[] getContent() {
         synchronized (mutex) {
             if (content == null) {