Clearing site data upon sign out in ASP.NET Core

Some time ago I've learned about new security header named Clear-Site-Data, but only recently I've had a chance to try it in action. The goal of the header is to provide a mechanism which allows developers to instruct a browser to clear a site’s data. This can be useful for example upon sign out, to ensure that locally stored data is removed. I wanted to explore that scenario in context of ASP.NET Core.

Before going further it's worth to mention that Clear-Site-Data is not supported yet by all browsers.

The Clear-Site-Data header

The header has a simple structure. Its value should be a comma separated list containing types of data to clear. The specification defines four of them:

  • "cache" which indicates that the server wishes to remove locally cached data
  • "cookies" which indicates that the server wishes to remove cookies
  • "storage" which indicates that the server wishes to remove data from local storages (localStorage, sessionStorage, IndexedDB, etc)
  • "executionContexts" which indicates that the server wishes to neuter and reload execution contexts

Additionally there is a wildcard pseudotype which indicates that the server wishes to remove all data types.

Based on the specification a simple class for generating the header value can be created. There is nothing particularly interesting about this class, so I will not show its implementation (for interested it can be found here). What I will show are helpers, which can be used to set the value on the response.

public static class HttpResponseHeadersExtensions
{
    public static void SetClearSiteData(this HttpResponse response, ClearSiteDataHeaderValue clearSiteData)
    {
        response.SetResponseHeader("Clear-Site-Data", clearSiteData?.ToString());
    }

    public static void SetWildcardClearSiteData(this HttpResponse response)
    {
        response.SetResponseHeader("Clear-Site-Data", "\"*\"");
    }

    internal static void SetResponseHeader(this HttpResponse response, string headerName, string headerValue)
    {
        if (!String.IsNullOrWhiteSpace(headerValue))
        {
            if (response.Headers.ContainsKey(headerName))
            {
                response.Headers[headerName] = headerValue;
            }
            else
            {
                response.Headers.Append(headerName, headerValue);
            }
        }
    }
}

Attaching to sign out in ASP.NET Core

Generating the header value and setting it on response was the easy part. The hard part is plugin this nicely into sign out functionality provided by ASP.NET Core. It is good to have such things as transparent as possible, so they don't have to become the required knowledge for all developers on the team. Plugging at a low level also helps when the operation is triggered by the framework without direct call from the application code.

After reviewing ASP.NET Core code related to authentication, the common call point seems to be SignOutAsync extension method available on HttpContext. The method is very simple.

public static Task SignOutAsync(this HttpContext context, string scheme, AuthenticationProperties properties) =>
    context.RequestServices.GetRequiredService<IAuthenticationService>().SignOutAsync(context, scheme, properties);

This would suggest that heart of everything is IAuthenticationService implementation. Extending that implementation with the additional behavior is an appealing idea. One way to do it is direct inheritance from AuthenticationService available in Microsoft.AspNetCore.Authentication.Core (this is the default implementation), but it would require making the new class aware of other authentication core concepts (like IAuthenticationSchemeProvider, IAuthenticationRequestHandler and IClaimsTransformation) for the sake of passing them to the basic implementation. Alternative approach would be to write a wrapper which takes an instance of class implementing IAuthenticationService as a parameter, passes all calls to it and introduces the new concepts. The added value of this approach is less coupling with implementation details (it only relies on the interface) and in the result ability to work with custom implementations as well.

internal class ClearSiteDataAuthenticationService : IAuthenticationService
{
    private readonly IAuthenticationService _authenticationService;
    private readonly ClearSiteDataHeaderValue _clearSiteDataHeaderValue;

    public ClearSiteDataAuthenticationService(IAuthenticationService authenticationService,
        ClearSiteDataHeaderValue clearSiteDataHeaderValue)
    {
        _authenticationService = authenticationService
                                 ?? throw new ArgumentNullException(nameof(authenticationService));
        _clearSiteDataHeaderValue = clearSiteDataHeaderValue
                                 ?? throw new ArgumentNullException(nameof(clearSiteDataHeaderValue));
    }

    public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
    {
        return _authenticationService.AuthenticateAsync(context, scheme);
    }

    ...

    public async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
    {
        await _authenticationService.SignOutAsync(context, scheme, properties);

        if (context.User?.Identity?.IsAuthenticated ?? false)
        {
            context.Response.SetClearSiteData(_clearSiteDataHeaderValue);
        }
    }
}

The last challenge is replacing the IAuthenticationService registered in services collection with the wrapper. Thankfully the IServiceCollection exposes the underlying ServiceDescriptors collection. This allows for locating currently registered descriptor, registering its implementation type under new one and setting an implementation factory which will take care of creating the wrappers. Following code does exactly that.

public static class ClearSiteDataAuthenticationServiceCollectionExtensions
{
    public static IServiceCollection AddClearSiteDataAuthentication(this IServiceCollection services,
        ClearSiteDataHeaderValue clearSiteDataHeaderValue)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }

        ServiceDescriptor authenticationServiceDescriptor = services
            .FirstOrDefault(d => d.ServiceType == typeof(IAuthenticationService));

        if (authenticationServiceDescriptor != null)
        {
            Type authenticationServiceImplementationType = authenticationServiceDescriptor.ImplementationType;
            ServiceLifetime authenticationServiceLifetime = authenticationServiceDescriptor.Lifetime;

            if (authenticationServiceImplementationType != null)
            {
                services.Remove(authenticationServiceDescriptor);

                services.Add(new ServiceDescriptor(
                    authenticationServiceImplementationType,
                    authenticationServiceImplementationType,
                    authenticationServiceLifetime)
                );

                services.Add(new ServiceDescriptor(
                    typeof(IAuthenticationService),
                    (IServiceProvider serviceProvider) =>
                    {
                        IAuthenticationService authenticationService = (IAuthenticationService)serviceProvider
                            .GetRequiredService(authenticationServiceImplementationType);

                        return new ClearSiteDataAuthenticationService(authenticationService, clearSiteDataHeaderValue);
                    },
                    authenticationServiceLifetime
                ));
            }
        }

        return services;
    }
}

Now all that needs to be done is calling AddClearSiteDataAuthentication after calling AddAuthentication.

All the code above can be found on GitHub.