Module: Data protection

5 of 13 Pages

Handle cookie consent requests

To handle the POST request generated by the widget, you’ll need a controller action.

However, this action requires some additional functionality to be added to the CookieConsentService from earlier.

Expand the CookieConsentService

You need a new method to facilitate the controller action that handles cookie preference data from the widget.

  1. Inject the following services into the CookieConsentService class.

    1. ICurrentCookieLevelProvider
    2. IConsentAgreementService
    3. IInfoProvider<ConsentInfo>
    4. ICookieAccessor
  2. Add a new SetCurrentCookieConsentLevel method.

  3. Asynchronously return a boolean that indicates if the method successfully updated the cookie level and consents.

  4. Adjust the value of the COOKIE_CONSENT_LEVEL cookie (trainingguides.cookieconsentlevel) if it has changed, and adjust the corresponding CMSCookieLevel cookie if necessary.Set the SameSite mode to Lax for the sake of this example

    In real world scenarios, you should decide on the properSameSite mode for each of your cookies depending on how and where it needs to be accessed. 

  5. Agree to the appropriate consents if the cookie level is raised and revoke the appropriate consents if the cookie level is lowered.

  6. If the consents were updated successfully and all cookies are up to date, set the COOKIE_ACCEPTANCE cookie (trainingguides.cookielevelselection) to true, indicating that the visitor has selected a cookie level.

C#
CookieConsentService.cs


...
/// <summary>
/// Sets current cookie consent level, internally sets system CookieLevel and agrees or revokes profiling consent.
/// </summary>
/// <param name="level">Cookie consent level to set</param>
/// <param name="mapping">Mapping of consent to cookie level when the visitor sets a cookie level from the widget</param>
/// <returns>true if the consents and cookie levels were updated according to the cookie level, false if there was an issue</returns>
public async Task<bool> SetCurrentCookieConsentLevel(CookieConsentLevel level, IDictionary<int, string> mapping)
{
    if (mapping == null || mapping.Count() == 0)
        return false;

    // Get original contact before changes to the cookie level
    var originalContact = ContactManagementContext.GetCurrentContact(false);

    bool cookiesUpToDate = UpdateCookieLevels(level);

    // Get current contact after changes to the cookie level
    var currentContact = ContactManagementContext.GetCurrentContact();

    bool preferedConsentsAgreed = await UpdatePreferredConsents(level, originalContact, currentContact, mapping);
    bool successful = preferedConsentsAgreed && cookiesUpToDate;

    if (successful)
        SetCookieAcceptanceCookie();

    return successful;
}

/// <summary>
/// Updates the visitor's cookie level to the provided value
/// </summary>
/// <param name<returns>true if the cookie level is equa="level">the cookie consent level to set for the visitor</param>
/// <returns>true if cookie levels were updated or alredy up-to-date, false if there is an exception</returns>
public bool UpdateCookieLevels(CookieConsentLevel level)
{
    // Get current cookie level and adjust it only if it has been changed
    var originalLevel = GetCurrentCookieConsentLevel();

    if (originalLevel == level)
        return true;
    try
    {
        // Set system cookie level according consent level
        SynchronizeCookieLevel(level);

        //Set cookie consent level into client's cookies
        cookieAccessor.Set(CookieNames.COOKIE_CONSENT_LEVEL, ((int)level).ToString(), new CookieOptions
        {
            Path = null,
            Expires = DateTime.Now.AddYears(1),
            HttpOnly = false,
            SameSite = SameSiteMode.Lax
        });
        return true;
    }
    catch
    {
        return false;
    }

}

/// <summary>
/// Agrees and revokes consents according to the preferred cookie level
/// </summary>
/// <param name="level">The cookie level</param>
/// <param name="originalContact">The ContactInfo object form before the cookie level was changed</param>
/// <param name="currentContact">The ContactInfo object from after the cookie level was changed</param>
/// <param name="mapping">The cookie level consent mapping</param>
/// <returns>true if the consents from the mapping can be found, false if not</returns>
private async Task<bool> UpdatePreferredConsents(CookieConsentLevel level, ContactInfo originalContact, ContactInfo currentContact, IDictionary<int, string> mapping)
{
    // Get consents
    var preferenceCookiesConsent = await consentInfoProvider.GetAsync(mapping[(int)CookieConsentLevel.Preference]);
    var analyticalCookiesConsent = await consentInfoProvider.GetAsync(mapping[(int)CookieConsentLevel.Analytical]);
    var marketingCookiesConsent = await consentInfoProvider.GetAsync(mapping[(int)CookieConsentLevel.Marketing]);

    if (preferenceCookiesConsent == null || analyticalCookiesConsent == null || marketingCookiesConsent == null)
        return false;

    // Agree cookie consents
    if (level >= CookieConsentLevel.Preference && currentContact != null)
    {
        if (!consentAgreementService.IsAgreed(currentContact, preferenceCookiesConsent))
            consentAgreementService.Agree(currentContact, preferenceCookiesConsent);
    }
    if (level >= CookieConsentLevel.Analytical && currentContact != null)
    {
        if (!consentAgreementService.IsAgreed(currentContact, analyticalCookiesConsent))
            consentAgreementService.Agree(currentContact, analyticalCookiesConsent);
    }
    if (level >= CookieConsentLevel.Marketing && currentContact != null)
    {
        if (!consentAgreementService.IsAgreed(currentContact, marketingCookiesConsent))
            consentAgreementService.Agree(currentContact, marketingCookiesConsent);
    }

    // Revoke consents
    if (level < CookieConsentLevel.Preference && originalContact != null)
    {
        if (consentAgreementService.IsAgreed(originalContact, preferenceCookiesConsent))
            consentAgreementService.Revoke(originalContact, preferenceCookiesConsent);
    }

    if (level < CookieConsentLevel.Analytical && originalContact != null)
    {
        if (consentAgreementService.IsAgreed(originalContact, analyticalCookiesConsent))
            consentAgreementService.Revoke(originalContact, analyticalCookiesConsent);
    }

    if (level < CookieConsentLevel.Marketing && originalContact != null)
    {
        if (consentAgreementService.IsAgreed(originalContact, marketingCookiesConsent))
            consentAgreementService.Revoke(originalContact, marketingCookiesConsent);
    }

    return true;
}

/// <summary>
/// Gets currently set cookie consent level.
/// </summary>
/// <returns>Cookie consent level</returns>
public CookieConsentLevel GetCurrentCookieConsentLevel()
{
    Enum.TryParse<CookieConsentLevel>(cookieAccessor.Get(CookieNames.COOKIE_CONSENT_LEVEL), out var consent);

    return consent;
}

/// <summary>
/// Synchronizes cookie level with consent level.
/// </summary>
/// <param name="level">Consent level</param>
private void SynchronizeCookieLevel(CookieConsentLevel level)
{
    switch (level)
    {
        case CookieConsentLevel.NotSet:
            SetCookieLevelIfChanged(cookieLevelProvider.GetDefaultCookieLevel());
            break;
        case CookieConsentLevel.Essential:
        case CookieConsentLevel.Preference:
        case CookieConsentLevel.Analytical:
            SetCookieLevelIfChanged(Kentico.Web.Mvc.CookieLevel.Visitor.Level);
            break;
        case CookieConsentLevel.Marketing:
            SetCookieLevelIfChanged(Kentico.Web.Mvc.CookieLevel.All.Level);
            break;
        default:
            throw new NotSupportedException($"CookieConsentLevel {level} is not supported.");
    }
}

/// <summary>
/// Sets CMSCookieLevel if it is different from the new one.
/// </summary>
/// <param name="newLevel">The new cookie level to which the contact should be set</param>
private void SetCookieLevelIfChanged(int newLevel)
{
    int currentCookieLevel = cookieLevelProvider.GetCurrentCookieLevel();

    if (newLevel != currentCookieLevel)
        cookieLevelProvider.SetCurrentCookieLevel(newLevel);
}

private void SetCookieAcceptanceCookie() =>
    cookieAccessor.Set(CookieNames.COOKIE_ACCEPTANCE, "true", new CookieOptions
    {
        Path = null,
        Expires = DateTime.Now.AddYears(1),
        HttpOnly = false,
        SameSite = SameSiteMode.Lax
    });
...

Remember to add all new public method signatures to the ICookieConsentService interface as well.

CMSCookieLevel

If Activity tracking is enabled, Xperience will track Page visits and Landing page activities for any site visitors with the Visitor cookie level or higher by default.

In our example, there are lesser consents that do not include activity tracking. However, Visitor cookie level is required to include the CurrentContact cookie and track which consents a visitor has accepted.

To ensure that these activities are tracked only when the cookie level is set to All, follow the steps described in the Enable activity tracking guide, where the tracking scripts are conditionally rendered to the page based on cookie level.

Always check with your legal team to ensure that the texts of your consents align with the functionality you map to these cookie levels.

Add the controller

With these new service methods, you can set up the controller to handle the POST request created by the form in the cookie preferences widget.

  1. Add a Features/DataProtection/Controllers folder containing a class called CookiesController to the TrainingGuides.Web project.
  2. Add an asynchronous action to the controller, registered to the /cookies/submit path specified by the widget.
    1. Decrypt the mapping and pass it to the CookieConsentService, along with the selected cookie level provided by the widget.
    2. Return the path to a partial view, showing a message that indicates success or failure, depending on the result. (The view will be created in a later step.)
C#
CookiesController.cs


using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using TrainingGuides.Web.Features.DataProtection.Services;
using TrainingGuides.Web.Features.DataProtection.Shared;
using TrainingGuides.Web.Features.DataProtection.Widgets.CookiePreferences;

namespace TrainingGuides.Web.Features.DataProtection.Controllers;

public class CookiesController : Controller
{
    private const string COOKIE_UPDATE_MESSAGE = "~/Features/DataProtection/Shared/CookieUpdateMessage.cshtml";
    private const string COOKIE_UPDATE_MESSAGE_SUCCESS = "Cookie consents have been successfully updated.";
    private const string COOKIE_UPDATE_MESSAGE_FAILURE = "Unable to update cookie consents. Please try again.";

    private readonly IStringEncryptionService stringEncryptionService;
    private readonly ICookieConsentService cookieConsentService;

    public CookiesController(
        IStringEncryptionService stringEncryptionService,
        ICookieConsentService cookieConsentService)
    {
        this.stringEncryptionService = stringEncryptionService;
        this.cookieConsentService = cookieConsentService;
    }

    [HttpPost("/cookies/submit")]
    public async Task<IActionResult> CookiePreferences(CookiePreferencesViewModel requestModel)
    {
        IDictionary<int, string> mapping;
        try
        {
            mapping = GetDictionaryMapping(requestModel.ConsentMapping);
        }
        catch
        {
            return ErrorView();
        }

        CookieConsentLevel selectedConsentValue;
        if (requestModel.CookieLevelSelected is > 0 and < 5)
        {
            selectedConsentValue = (CookieConsentLevel)requestModel.CookieLevelSelected;
        }
        else
        {
            return ErrorView();
        }

        try
        {
            if (!await cookieConsentService.SetCurrentCookieConsentLevel(selectedConsentValue, mapping))
            {
                throw new Exception();
            }
        }
        catch
        {
            return ErrorView();
        }

        return SuccessView(COOKIE_UPDATE_MESSAGE_SUCCESS);
    }

    /// <summary>
    /// Gets a dictionary of consent codenames and the cookie levels to which they are mapped from an encrypted string
    /// </summary>
    /// <param name="mappingEncrypted">The encrypted string representation of the mapping</param>
    /// <returns>A dictionary of integer cookie levels and consent codename values</returns>
    /// <exception cref="Exception">Throws if there is no encrypted string, or if the dictionary can't be decrypted and deserialized, or if the mapping does not contain the required cookie level keys</exception>
    private IDictionary<int, string> GetDictionaryMapping(string mappingEncrypted)
    {
        if (string.IsNullOrEmpty(mappingEncrypted))
        {
            throw new Exception("No encrypted string.");
        }

        Dictionary<int, string> consentMapping;

        try
        {
            string mappingDecrypted = stringEncryptionService.DecryptString(mappingEncrypted);
            consentMapping = JsonConvert.DeserializeObject<Dictionary<int, string>>(mappingDecrypted) ?? [];
        }
        catch
        {
            throw new Exception("Dictionary can't be decrypted or deserialized.");
        }

        if (!(consentMapping.ContainsKey((int)CookieConsentLevel.Preference)
            && consentMapping.ContainsKey((int)CookieConsentLevel.Analytical)
            && consentMapping.ContainsKey((int)CookieConsentLevel.Marketing)))
        {
            throw new Exception("Mapping does not contain the required cookie level keys.");
        }

        return consentMapping;
    }

    /// <summary>
    /// Gets a list of consent codenames from an encrypted string
    /// </summary>
    /// <param name="mappingEncrypted">The encrypted string representation of the consents list</param>
    /// <returns>A list of consent codenames</returns>
    /// <exception cref="Exception">Throws if there is no encrypted string, or if no consents are found from decryption</exception>
    private IEnumerable<string> GetConsentsList(string mappingEncrypted)
    {
        if (string.IsNullOrEmpty(mappingEncrypted))
        {
            throw new Exception();
        }

        string mapping = stringEncryptionService.DecryptString(mappingEncrypted);

        IEnumerable<string> consents = mapping.Split(Environment.NewLine).ToList();

        if (consents.Count() == 0)
        {
            throw new Exception();
        }

        return consents;
    }

    private IActionResult SuccessView(string message = "") =>
        PartialView(COOKIE_UPDATE_MESSAGE, new CookieUpdateMessageViewModel(message));

    private IActionResult ErrorView() =>
        PartialView(COOKIE_UPDATE_MESSAGE, new CookieUpdateMessageViewModel(COOKIE_UPDATE_MESSAGE_FAILURE));
}

Finally, in the TrainingGuides.Web/Features/DataProtection/Shared folder, create the partial view referenced in the controller above and its view model.

cshtml
CookieUpdateMessage.cshtml


@using TrainingGuides.Web.Features.DataProtection.Shared
@using Microsoft.AspNetCore.Html

@model CookieUpdateMessageViewModel

@{
    HtmlString messageHtml = new HtmlString($"<div class=\"cookies__message\">{Model.Message}</div>");
}

@messageHtml

C#
CookieUpdateMessageViewModel.cs


namespace TrainingGuides.Web.Features.DataProtection.Shared;

public class CookieUpdateMessageViewModel
{
    public string Message { get; set; }

    public CookieUpdateMessageViewModel(string message)
    {
        Message = message;
    }
}

Use the widget

Now, the widget is available for use.

  1. Log in to the admin interface of Xperience by Kentico and open the Training guides pages application.
  2. Navigate to the Cookie policy page, on the Page Builder tab, and choose Edit page → Create a new version.
  3. Add the Cookie preferences widget to the widget zone, setting a title and description for essential cookies, and a label for the submit button.
  4. Save and Publish the page.

Now, users who visit the page can configure their cookie level, as shown in the video below: