Skip to main content

Strict Transport Security in ASP.NET MVC. This ASP.NET MVC attribute that forces an unsecured HTTP request to be re-sent over HTTPS and adds HSTS headers to secured requests.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;

// article: http://tpeczek.blogspot.com/2015/07/strict-transport-security-in-aspnet-mvc.html
// source: https://github.com/tpeczek/Lib.Web.Mvc/blob/master/Lib.Web.Mvc/RequireHstsAttribute.cs

namespace Mvc
{
    /// <summary>
    /// Represents an attribute that forces an unsecured HTTP request to be
    /// re-sent over HTTPS and adds HSTS headers to secured requests.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class RequireHstsAttribute : RequireHttpsAttribute
    {
        private readonly uint _maxAge;
        private const string _strictTransportSecurityHeader = "Strict-Transport-Security";
        private const string _maxAgeDirectiveFormat = "max-age={0}";
        private const string _includeSubDomainsDirective = "; includeSubDomains";
        private const string _preloadDirective = "; preload";
        private const int _minimumPreloadMaxAge = 10886400;

        /// <summary>
        /// Gets the time (in seconds) that the browser should remember that
        /// this resource is only to be accessed using HTTPS.
        /// </summary>
        public uint MaxAge { get { return _maxAge; } }

        /// <summary>
        /// Gets or sets the value indicating if this rule applies to all
        /// subdomains as well.
        /// </summary>
        public bool IncludeSubDomains { get; set; }

        /// <summary>
        /// Gets or sets the value indicating if subscription to HSTS preload
        /// list (https://hstspreload.appspot.com/) should be confirmed.
        /// </summary>
        public bool Preload { get; set; }

        /// <summary>
        /// Initializes a new instance of the RequireHstsAttribute class.
        /// </summary>
        /// <param name="maxAge">The time (in seconds) that the browser
        /// should remember
        /// that this resource is only to be accessed using HTTPS.</param>
        public RequireHstsAttribute(uint maxAge) : base()
        {
            _maxAge = maxAge;
            IncludeSubDomains = false;
            Preload = false;
        }

        /// <summary>
        /// Determines whether a request is secured (HTTPS). If it is sets the
        /// Strict-Transport-Security header. If it is not calls the
        /// HandleNonHttpsRequest method.
        /// </summary>
        /// <param name="filterContext"></param>
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.HttpContext.Request.IsSecureConnection)
            {
                if (Preload && (MaxAge < _minimumPreloadMaxAge))
                {
                    throw new InvalidOperationException("In order to confirm HSTS preload list subscription expiry must be at least eighteen weeks (10886400 seconds).");
                }

                if (Preload && !IncludeSubDomains)
                {
                    throw new InvalidOperationException("In order to confirm HSTS preload list subscription subdomains must be included.");
                }

                StringBuilder headerBuilder = new StringBuilder();
                headerBuilder.AppendFormat(_maxAgeDirectiveFormat, _maxAge);

                if (IncludeSubDomains)
                {
                    headerBuilder.Append(_includeSubDomainsDirective);
                }

                if (Preload)
                {
                    headerBuilder.Append(_preloadDirective);
                }

                filterContext.HttpContext.Response.AppendHeader(_strictTransportSecurityHeader, headerBuilder.ToString());
            }
            else
            {
                HandleNonHttpsRequest(filterContext);
            }
        }

    }
}