Skip to main content

ASP.NET MVC Filter that represents an attribute to force an unsecured HTTP request, to be re-sent over HTTPS.

namespace Boilerplate.Web.Mvc.Filters
{
    using System;
    using System.Net;
    using System.Web;
    using System.Web.Mvc;

    /// <summary>
    /// Represents an attribute that forces an unsecured HTTP request to be re-sent over HTTPS.
    /// <see cref="RequireHttpsAttribute"/> performs a 302 Temporary redirect from a HTTP URL to a HTTPS URL. This
    /// filter gives you the option to perform a 301 Permanent redirect or a 302 temporary redirect. You should
    /// perform a 301 permanent redirect if the page can only ever be accessed by HTTPS and a 302 temporary redirect if
    /// the page can be accessed over HTTP or HTTPS. <see cref="RequireHttpsAttribute"/> also throws an
    /// <see cref="InvalidOperationException"/> if request is made except GET, which returns a 500 Internal Server
    /// Error to the client. This filter, returns a 405 Method Not Allowed instead, which is much more suitable.
    /// </summary>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
    public class RedirectToHttpsAttribute : FilterAttribute, IAuthorizationFilter
    {
        private readonly bool permanent;

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectToHttpsAttribute"/> class.
        /// </summary>
        /// <param name="permanent">if set to <c>true</c> the redirection should be permanent; otherwise,
        /// <c>false</c>.</param>
        public RedirectToHttpsAttribute(bool permanent)
        {
            this.permanent = permanent;
        }

        /// <summary>
        /// Gets a value indicating whether the redirection should be permanent.
        /// </summary>
        /// <value>
        /// <c>true</c> if the redirection should be permanent; otherwise, <c>false</c>.
        /// </value>
        public bool Permanent
        {
            get { return this.permanent; }
        }

        /// <summary>
        /// Determines whether a request is secured (HTTPS) and, if it is not, calls the
        /// <see cref="HandleNonHttpsRequest"/> method.
        /// </summary>
        /// <param name="filterContext">An object that encapsulates information that is required in order to use the
        /// <see cref="RequireHttpsAttribute"/> attribute.</param>
        /// <exception cref="ArgumentNullException">The <paramref name="filterContext"/> parameter is <c>null</c>.</exception>
        public virtual void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (!filterContext.HttpContext.Request.IsSecureConnection)
            {
                this.HandleNonHttpsRequest(filterContext);
            }
        }

        /// <summary>
        /// Handles unsecured HTTP requests that are sent to the action method.
        /// </summary>
        /// <param name="filterContext">An object that encapsulates information that is required in order to use the
        /// <see cref="RequireHttpsAttribute"/> attribute.</param>
        /// <exception cref="HttpException">The HTTP request contains an invalid transfer method override.
        /// All GET requests are considered invalid. A HTTP 405 Method Not Allowed is thrown.</exception>
        protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext)
        {
            // Only redirect for GET requests, otherwise the browser might not propagate the verb and request body correctly.
            if (!string.Equals(
                filterContext.HttpContext.Request.HttpMethod,
                WebRequestMethods.Http.Get,
                StringComparison.OrdinalIgnoreCase))
            {
                // The RequireHttpsAttribute throws an InvalidOperationException. Some bots and spiders make HEAD
                // requests (to reduce bandwidth) and we don't want them to see a 500-Internal Server Error. A 405
                // Method Not Allowed would be more appropriate.
                throw new HttpException((int)HttpStatusCode.Forbidden, "Forbidden");
            }

            string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url, this.permanent);
        }
    }
}