HTTP Cookies and ASP.NET Web API

HTTP Cookies are bits of information sent by an HTTP Server in an HTTP Response. So when a browser is said to ‘drop a cookie’, it basically implies that the ‘HTTP Client’ (browser in this case) received a Cookie from the server and has persisted it on the client. The ‘HTTP Client’ then returns this cookie to the server via the Cookie Header in subsequent requests.

To dig in just a little bit further, scope of an HTTP Cookie is controlled by attributes set in the cookie header.

- Expires: A Date/Time value indicating when the cookie should expire. The client deletes the cookie once it expires.

- Max-Age: A numeric value representing a timespan span indicating life of the cookie. Like in case of Expires, the cookie is deleted once it reaches it maximum age. Between Expires and Max-Age, Age takes precedence. If neither is set, client deletes the cookie once the ‘session’ expires

- Domain: Specifies the domain which receives the cookie. If not specified, domain is the origin server.

- Path: Limits the cookie to the specified path within the domain. If not specified, the URI’s path is used.

Having seen what a cookie can store comes the biggest caveat about a HTTP cookie. Fact is, HTTP clients can completely ignore a cookie. This makes them an unreliable state saving mechanism.

Cookies and Web API

Web API as we know is meant for building services over HTTP. These services can be consumed by any type of client. It could be a web page making AJAX requests or it could be a headless bot polling for data or it could be a native app fetching data. Given the various types of clients that can access a Web API endpoint, relying on cookies to get back session related information is not a particularly good design choice.

Having said that, Web API does work over HTTP and cookies are a part of the HTTP spec. So for whatever narrow case you may need your Web API services to support cookies, you can definitely do that. But we will pepper it with lots of ‘watch out’, ‘don’t do it’ and ‘I told you so’ flags!!!

Setting a Cookie

We can set a Cookie in the HttpResponseMessage for a Web API Get request. We use the CookieHeaderValue object as follows

public HttpResponseMessage Get()
{
HttpResponseMessage respMessage = new HttpResponseMessage();
respMessage.Content = new ObjectContent<string []>
   (new string[] { "value1", "value2" }, new JsonMediaTypeFormatter());
CookieHeaderValue cookie = new CookieHeaderValue("session-id", "12345");
cookie.Expires = DateTimeOffset.Now.AddDays(1);
cookie.Domain = Request.RequestUri.Host;
cookie.Path = "/";
respMessage.Headers.AddCookies(new CookieHeaderValue[] { cookie });
return respMessage;
}


The CookieHeaderValue object is defined in the System.Net.Http.Header namespace.

The JsonMediaTypeFormatter is defined in System.Net.Http.Formatters namespace.

If we start monitoring traffic using Fiddler, and hit the URL http://localhost:[yourport]/api/values/ we will see the following

fiddler-single-cookie

As we can see, the cookie that we had added got posted to us.

Setting a Cookie with multiple values

If you want to setup multiple values in a cookie instead of multiple cookies you can use name value pairs and stuff them into one cookie as follows

public HttpResponseMessage Get()
{
HttpResponseMessage respMessage = new HttpResponseMessage();
respMessage.Content = new ObjectContent<string[]>(new string[]
{
  "value1",
  "value2"
}, new JsonMediaTypeFormatter());
var nvc = new NameValueCollection();
nvc["sessid"] = "1234";
nvc["3dstyle"] = "flat";
nvc["theme"] = "red";
var cookie = new CookieHeaderValue("session", nv);
cookie.Expires = DateTimeOffset.Now.AddDays(1);
cookie.Domain = Request.RequestUri.Host;
cookie.Path = "/";
respMessage.Headers.AddCookies(new CookieHeaderValue[] { cookie });
return respMessage;
}


If we watch the response on fiddler we can see the Name Value pairs come in separated by ampersand (&).

fiddler-single-cookie-multiple-values

Now if we were to post the cookie back to the server, we can retrieve it as follows:

public void Post([FromBody]string value)
{
string sessionId = "";
string style = "";
string theme = "";
CookieHeaderValue cookie = Request.Headers.GetCookies("session").FirstOrDefault();
if (cookie != null)
{
  CookieState cookieState = cookie["session"];
  sessionId = cookieState["sessid"];
  style = cookieState["3dstyle"];
  theme = cookieState["theme"];
}
}


We can see the values as follows:

cookie-valuess-returned

Setting Cookies in Web API Handlers

One can also create a DelegationHandler to inject cookies outside the Controller. Since Requests go to controllers via the handler and response goes out via the handler, a custom delegation handler becomes the right place to add Application Specific cookies. For example if we want to stamp our cookies with a key that comprises of a custom string and a GUID, we could build it as follows

public class RequestStampCookieHandler : DelegatingHandler
{
static public string CookieStampToken = "cookie-stamp";
protected async override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(
  HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
  string cookie_stamp;
  var cookie = request.Headers.GetCookies(CookieStampToken).FirstOrDefault();
  if (cookie == null)
  {
   cookie_stamp = "COOKIE_STAMPER_" +Guid.NewGuid().ToString();
  }
  else
  {
   cookie_stamp = cookie[CookieStampToken].Value;
   try
   {
    Guid guid = Guid.Parse(cookie_stamp.Substring(22));
   }
   catch (FormatException)
   {
    // Invalid Stamp! Create a new one.
    cookie_stamp = "COOKIE_STAMPER_" + Guid.NewGuid().ToString();
   }
  }
  // Store the session ID in the request property bag.
  request.Properties[CookieStampToken] = cookie_stamp;
  // Continue processing the HTTP request.
  HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
  // Set the session ID as a cookie in the response message.
  response.Headers.AddCookies(new CookieHeaderValue[] {
   new CookieHeaderValue(CookieStampToken, cookie_stamp)
  });
  return response;
}
}


Now if we run the application and try to access the API, we’ll receive our ‘COOKIE_STAMPER_*’ cookie in the response.

fiddler-delegation-handler-cookie

Weird Behavior Alert: If you see above, there is only one Cookie here. But I have not removed any code from the controller so ideally we should have had two cookies. This is something weird I encountered. If you encounter it, a hack around it is to push the same cookie with different tokens in the delegation handler and magically all three cookies reappear. I am putting this down as a sync bug in AddCookies for now, will circle with the WebAPI team to see if it’s a known issue or I missed something.

delegation-handler-issue

Conclusion

To wrap up, we saw how we could use cookies to exchange bits of information between a Web API service and a client. However, the biggest caveat is that Cookie data should not trusted as it is prone to tampering or complete removal (in case cookies are disabled).

In the narrow band of possible requirements that may need Cookies, we saw how to use them here.

Download the entire source code of this article (Github)

3 comments:

  1. This is an excellent article. i have tried to download the code and ran. i am getting the response, But i am not able to see the cookies on the header. Do i need to do some settings in my local environment? Appreciate your response.

    ReplyDelete
  2. I have a question. Is it possible to retrieve the value of cookie from API controller to MVC controller? Thanks for answering.

    ReplyDelete
  3. @Schnee Haven't tried it but use the CookieContainer object of the WebRequestHandler. Use its GetCookies() method.

    ReplyDelete