Implementing European Cookie Law compliance in ASP.NET MVC

In Europe, and especially the Netherlands, there are stricter legal requirements for the privacy of consumers than in most other countries. There is the well-known european “cookie law”, that was implemented very eagerly by the Dutch government in the “cookiewet”, requiring websites to explicitly ask visitors for permission before they start tracking them and share data with third parties. In the Netherlands there are also legal requirements for storing and processing of any personally identifiable data: every company that collects such data must register this with a government organization (“college bescherming persoonsgegevens”) or a recognized trade association. Companies must also clearly communicate the purpose for collecting consumer data and may not use the data for anything else than this stated purpose.

I think this is all very well intended, however the problem is that the internet is developing on a global scale (with the US setting the norm) and countries outside of western Europe (especially the US) seem to not care as much for the privacy of citizens. Other countries have fewer regulations limiting what companies can do. Last year, when the cookie law went into effect, most Dutch websites implemented it by showing a popup or notification simply telling the user to accept cookies or go away (in somewhat nicer words). Users quickly learned that refusing cookies usually leads to a dead-end page so everyone now clicks the message away by accepting the cookies, feeling annoyed by it. Recently, the Dutch government announced the intention to relax their interpretation of the law somewhat: after the first page with the message, clicking anything other than the cookie refusal link may be interpreted as implicitly allowing cookies. This will make it less annoying.

Ok, so before we start implementing Facebook Like buttons and other features that come with a privacy cost to our visitors, we need to make sure we can comply with this cookie law. The funny thing is, to implement the cookie law, we will use a cookie! It is important to understand that the cookie law actually does not even mention the word cookie, it applies to every technical means available to identify a user. So you cannot code around it, using Local Storage, Flash Shared Objects or similar techniques. The law by itself is not unreasonable: it does not require consent for cookies that are technically required for the primary function of the website. Session cookies are allowed for login or multipage forms and we can also use a persistent (first party) cookie to remember cookie-consent, without asking consent for that.

We’ll implement the cookie law like this:

  • On every incoming request, we will check for the presence of a cookie named CookieConsent
  • If the cookie is not found, we check for presence of the Do Not Track header
  • If the user sends a Do Not Track setting in the request headers, we respect it silently and don’t ask for consent and don’t set the ConsentCookie
  • If Do Not Track is not set, we’ll set a session cookie CookieConsent=asked and show the consent buttons
  • We will handle the user response to the cookie consent message accordingly. If the user allows or denies consent, we will set the ConsentCookie value to true or false, with an expiration of 1 year
  • If we do not get an explicit response to the cookie consent message, on the next incoming requests, we’ll check for this cookie:
  • If the value is “asked”, we set it to “true” in the next response, interpreting this as implicit consent
  • If the value is any other value, we’ll leave it unchanged and assume no consent
  • To the application, we’ll make available a static utility function indicating the CookieConsent value so the Views can easily decide whether to include impacted functionality

I think this is a very fair way of handling user privacy on a website. Websites that are dependent on advertising income and affiliate marketing may feel this implementation is unfavorable to them, and drag their feet a little bit, i.e. by ignoring the Do Not Track header or by not allowing their users to view content when they deny consent. You can easily do that if you wish and you’ll probably get away with it, too. But I think this implementation is the most user-friendly and “right” way to do it.

In ASP.NET, the best way to implement a system wide function like this is with a FilterAttribute. Here is the CookieConsentAttribute class:


/*
* ASP.NET ActionFilterAttribute to help implement EU Cookie-law
* MIT Licence (c) Maarten Sikkema, Macaw Nederland BV
*/

using System;
using System.Web;
using System.Web.Mvc;

namespace Auction.Web.Utility
{
/// <summary>
/// ASP.NET MVC FilterAttribute for implementing european cookie-law
/// </summary>
public class CookieConsentAttribute : ActionFilterAttribute
{
public const string CONSENT_COOKIE_NAME = "CookieConsent";

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var viewBag = filterContext.Controller.ViewBag;
viewBag.AskCookieConsent = true;
viewBag.HasCookieConsent = false;

var request = filterContext.HttpContext.Request;

// Check if the user has a consent cookie
var consentCookie = request.Cookies[CONSENT_COOKIE_NAME];
if (consentCookie == null)
{
// No consent cookie. We first check the Do Not Track header value, this can have the value "0" or "1"
string dnt = request.Headers.Get("DNT");

// If we receive a DNT header, we accept its value and do not ask the user anymore
if (!String.IsNullOrEmpty(dnt))
{
viewBag.AskCookieConsent = false;
if (dnt == "0")
{
viewBag.HasCookieConsent = true;
}
}
else
{
if (IsSearchCrawler(request.Headers.Get("User-Agent")))
{
// don't ask consent from search engines, also don't set cookies
viewBag.AskCookieConsent = false;
}
else
{
// first request on the site and no DNT header.
consentCookie = new HttpCookie(CONSENT_COOKIE_NAME);
consentCookie.Value = "asked";
filterContext.HttpContext.Response.Cookies.Add(consentCookie);
}
}
}
else
{
// we received a consent cookie
viewBag.AskCookieConsent = false;
if (consentCookie.Value == "asked")
{
// consent is implicitly given
consentCookie.Value = "true";
consentCookie.Expires = DateTime.UtcNow.AddYears(1);
filterContext.HttpContext.Response.Cookies.Set(consentCookie);
viewBag.HasCookieConsent = true;
}
else if (consentCookie.Value == "true")
{
viewBag.HasCookieConsent = true;
}
else
{
// assume consent denied
viewBag.HasCookieConsent = false;
}
}
base.OnActionExecuting(filterContext);
}

private bool IsSearchCrawler(string userAgent)
{
if (!userAgent.IsNullOrEmpty())
{
string[] crawlers = new string[]
{
"Baiduspider",
"Googlebot",
"YandexBot",
"YandexImages",
"bingbot",
"msnbot",
"Vagabondo",
"SeznamBot",
"ia_archiver",
"AcoonBot",
"Yahoo! Slurp",
"AhrefsBot"
};
foreach (string crawler in crawlers)
if (userAgent.Contains(crawler))
return true;
}
return false;
}
}


/// <summary>
/// Helper class for easy/typesafe getting the cookie consent status
/// </summary>
public static class CookieConsent
{
public static void SetCookieConsent(HttpResponseBase response, bool consent)
{
var consentCookie = new HttpCookie(CookieConsentAttribute.CONSENT_COOKIE_NAME);
consentCookie.Value = consent ? "true" : "false";
consentCookie.Expires = DateTime.UtcNow.AddYears(1);
response.Cookies.Set(consentCookie);
}

public static bool AskCookieConsent(ViewContext context)
{
return context.ViewBag.AskCookieConsent ?? false;
}

public static bool HasCookieConsent(ViewContext context)
{
return context.ViewBag.HasCookieConsent ?? false;
}
}
}
view rawCookieConsentAttribute.cs hosted with ❤ by GitHub


We can use the attribute by simply setting it on the HomeController:


using Auction.Web.Utility;
using System.Web.Mvc;

namespace Auction.Web.Controllers
{
[CookieConsent]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult About()
{
ViewBag.Message = "Your app description page.";
return View();
}

public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
view rawHomeController.cs hosted with ❤ by GitHub


Now we have our environment working and we can implement the message and handle the responses. I am going to implement it the way the law is intended, and will try not to sabotage it. This means I will still allow users who declined cookies on my site, and will disable any functionality that might compromise privacy, like Tracking Cookies, Affiliate Marketing programs and Social Buttons.

The consent message is in a partial view. It has two buttons, that are each simple submit buttons in their own <form>. This is to make sure it will always work; even with javascript disabled. A small javascript asyncform.js is used to submit the “decline” form as an Ajax request is javascript is enabled. The “accept” button is not sent as an Ajax request, because the page needs to be refreshed after a user accepts cookies to show the extra features. The form is submitted to SiteController that uses the CookieConsent helper class to set the consent cookie to the value specified by the user.


<div id="cookie-alert">
<div class="alert">
<p><strong>Cookies:</strong> please allow us to use cookies for Facebook Like buttons and Google analytics.</p>
<form class="btn-group-vertical" action="@Url.Action("NoCookies", "Site", new { ReturnUrl = Request.RawUrl })" method="post" data-async="true" data-target="#cookie-alert" >
<input type="submit" name="decline" class="btn btn-warning" value="Decline" />
</form>
<form class="btn-group-vertical" action="@Url.Action("AllowCookies", "Site", new { ReturnUrl = Request.RawUrl })" method="post">
<input type="submit" name="allow" class="btn btn-success" value="Allow" />
</form>
</div>
</div>
view raw_CookieConsentMessage.cshtml hosted with ❤ by GitHub
$('form[data-async=true]').on('submit', function (event) {
var $form = $(this);
var $target = $($form.attr('data-target'));

$.ajax({
type: $form.attr('method'),
url: $form.attr('action'),
data: $form.serialize(),

success: function (data, status) {
$target.html(data);
}
});

event.preventDefault();
});
view rawasyncform.js hosted with ❤ by GitHub
using Auction.Web.Utility;
using System.Web.Mvc;

namespace Auction.Web.Controllers
{
[AllowAnonymous]
public class SiteController : BaseController
{
public ActionResult AllowCookies(string ReturnUrl)
{
CookieConsent.SetCookieConsent(Response, true);
return RedirectToLocal(ReturnUrl);
}

public ActionResult NoCookies(string ReturnUrl)
{
CookieConsent.SetCookieConsent(Response, false);
// if we got an ajax submit, just return 200 OK, else redirect back
if (Request.IsAjaxRequest())
return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK);
else
return RedirectToLocal(ReturnUrl);
}


[OutputCache(Duration = 60 * 60 * 24 * 365, Location = System.Web.UI.OutputCacheLocation.Any)]
public ActionResult FacebookChannel()
{
return View();
}
}
}
view rawSiteController.cs hosted with ❤ by GitHub


I will include the cookie consent message in _Layout.cshtml, so that it will appear on every page. I placed it on the top of the page, right below the menu:


@using Auction.Web.Utility
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>@ViewBag.Title - My ASP.NET MVC Application</title>
@RenderSection("meta", false)
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<link href="~/Content/less/bootstrap.less" rel="stylesheet">
<style type="text/css" >
body {
padding-top: 60px;
padding-bottom: 40px;
}
</style>
<link href="~/Content/less/responsive.less" rel="stylesheet">

@Html.Partial("_html5shiv")

<!-- Fav and touch icons -->
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="~/Content/ico/apple-touch-icon-144-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="~/Content/ico/apple-touch-icon-114-precomposed.png">
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="~/Content/ico/apple-touch-icon-72-precomposed.png">
<link rel="apple-touch-icon-precomposed" href="~/Content/ico/apple-touch-icon-57-precomposed.png">
<link href="~/Content/ico/favicon.ico" rel="shortcut icon" type="image/x-icon" />
</head>
<body>
@Html.Partial("_NavBar")

<div class="container">
@if (CookieConsent.AskCookieConsent(ViewContext))
{
@Html.Partial("_CookieConsentMessage")
}
@RenderSection("featured", required: false)
@RenderBody()
<hr />
<div class="footer">
<p>&copy; Company 2013</p>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")

@if (CookieConsent.HasCookieConsent(ViewContext))
{
@Html.Partial("_FacebookInit")
}

@RenderSection("scripts", required: false)
</div>
</body>
</html>
view raw_Layout.cshtml hosted with ❤ by GitHub


Near the bottom of _Layout.cshtml you can see how I use the static CookieConsent.HasCookieConsent() method to decide whether to include the facebook javascript initialization script. I’ll continue on Facebook integration in another post.

The resulting page looks like this, on the first request. On the next request, the message will disappear and depending on the input some optional features may be disabled. Using Chrome Developer tools we can delete the cookie, disable javascript and try all the possible scenario’s.



image Nice: we are now compliant with the Dutch (and European) cookie law according to its most recent interpretation.

[This post is part of the series Development of a mobile website with apps and social features]