Blog Article:

How to force pages to *not* use SSL in ASP.NET MVC by creating a ForbidHttpsAttribute

by Joel Marshall on February 24, 2015

Here's a quick one for you.

There are times when you want to require a page be transmitted over TLS (transport layer security) using SSL (secure sockets layer). Anytime you are transmitting sensitive information over the wire, requiring HTTPS is a good idea. We actually require SSL sitewide now because Google encourages it. We'll assume that you already know about the reasons for encrypting traffic and when to do so. However, there are times when you want just the opposite- you want requests to a page to *not* use TLS, just plain old HTTP.

So what are the reasons that you would want to to this? Well, if you're reading this you probably already have one, but here are a few examples:

  • HTTP and HTTPS versions of the same page can be treated as duplicate content by search engines. If you're not ready to take the plunge to all HTTPS all the time, you should make sure each page sticks to one protocol or the other. There are other ways to mitigate this such as using rel="canonical" meta tags, but that's for another article.
  • If you're mixing HTTP and HTTPS on the same page, some services will treat each URLs served under each protocol as different content even if that's not what you intended. AddThis, which we use on this site, is just one such example. Since most of our content was served over HTTP until recently, when we switched to all HTTPS our share counts were basically reset :(
  • Some services just don't support HTTPS. Sometimes this is due to privacy concerns, other times the service might use a Freemium model where you have to pay up to get their service to work over SSL.
  • There is a performance hit for using SSL. As server hardware has become more powerful that hit has become less substantial, but it hasn't gone away nor will it. The server will always have to decrypt requests and encrypt responses for SSL- there's no getting around that. As the number of users on a site becomes large, those little penalties can really add up. That's why, for instance, Stack Exchange still doesn't use https by default.

Ok, I said this would be quick, so let's get to it. How do we force HTTP "The Right WayTM" In ASP.NET MVC? There's a RequireHttpsAttribute baked in, but there's no opposite of that attribute. So what to do? Well, as it turns out the code for RequireHttpsAttribute is pretty short. Here it is in its entirety:

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;
using System.Web.Mvc.Properties;

namespace System.Web.Mvc
{
    [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed because type contains virtual extensibility points.")]
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class RequireHttpsAttribute : FilterAttribute, IAuthorizationFilter
    {
        public virtual void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

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

        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, "GET", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl);
            }

            // redirect to HTTPS version of page
            string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url);
        }
    }
}

This is pretty easy to follow. Basically, if the request is using SSL and it's a GET request, issue a redirect to the same page using HTTPS. So if we want to forbid a request from using SSL, we just flip the above code on its head (we cleaned this up a bit as well):

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ForbidHttpsAttribute : FilterAttribute, IAuthorizationFilter
{
    public virtual void OnAuthorization(AuthorizationContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException("filterContext");
        }

        if (filterContext.HttpContext.Request.IsSecureConnection)
        {
            HandleHttpsRequest(filterContext);
        }
    }

    protected virtual void HandleHttpsRequest(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, "GET", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("The requested resource can only be accessed *without* SSL.");
        }

        // redirect to HTTP version of page
        var url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
        filterContext.Result = new RedirectResult(url);
    }
}

And that's it as far as the code. Use the ForbidHttpsAttribute just like you would use the RequireHttpsAttribute. Don't forget, however, that there are security implications that you need to keep in mind if you use this. If you require a page to be transmitted without TLS, everything will go over the wire in cleartext. This might not be what you want, especially if you're using cookies or bearer tokens for authentication.

If you're using OWIN cookie authentication, you can mitigate this by setting the CookieSecure property on the CookieAuthenticationOptions object to CookieSecureOption.Always. That will set the auth cookie's secure flag and it will only be transmitted on requests that go over HTTPS. If you're using bearer tokens for auth, the mechanism for making sure the header isn't added to HTTP requests is up to you. Keep in mind that with either of these methods the server will assume that the user is unauthenticated on HTTP requests.

As we've outlined in this article, forcing HTTP isn't hard but it's not something you should just flip on without giving it some consideration. If you *need* to force HTTP for some of your content and you need HTTPS elsewhere, think it through so that you don't end up trading one set of pitfalls for another.

If you liked this article, please share it!

Joel Marshall

Founder

Joel has been developing applications with Microsoft ASP.NET and related web technologies for 15 years. He is passionate about software architecture, design patterns, and emerging web technologies.