Entering and exiting HTTPS with ASP.NET MVC
I think I’ve found a relatively painless and DRY way to apply SSL/HTTPS selectively to certain actions in an ASP.NET MVC application.
I’ve looked into several approaches including Steve Sanderson’s self-admittedly hack-ish approach which I won’t go into here because it’s an excellent article revealing why this is a difficult problem with ASP.NET. Steve’s is probably the ideal approach, but shortcomings in System.Web.Routing prevent you from doing it cleanly. It’s well worth the read.
There are also quite a few other methods which use various methods to generate links with the correct scheme, but they involve changing all your views and generally repeating yourself all over the place - they work but they’re not pretty.
As you might know, MVC 2 now provides a [RequireHttps] attribute - so entering HTTPS is pretty much a solved problem. The problem with this attribute is that once you’re in HTTPS mode, you stay there. So how to exit?
My solution is to implement an action filter called [ExitHttpsIfNotRequired] which can be applied to a controller or action and automatically redirects to HTTP if [RequireHttps] isn’t also applied.
Here it is:
public class ExitHttpsIfNotRequiredAttribute : FilterAttribute, IAuthorizationFilter {
public void OnAuthorization(AuthorizationContext filterContext) {
// abort if it's not a secure connection
if(!filterContext.HttpContext.Request.IsSecureConnection) return;
// abort if a [RequireHttps] attribute is applied to controller or action
if(filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Length > 0) return;
if(filterContext.ActionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Length > 0) return;
// abort if a [RetainHttps] attribute is applied to controller or action
if(filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true).Length > 0) return;
if(filterContext.ActionDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true).Length > 0) return;
// abort if it's not a GET request - we don't want to be redirecting on a form post
if(!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) return;
// redirect to HTTP
string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
[RetainHttps] is basically an empty attribute which can be used to signal to [ExitHttpsIfNotRequired] to not mess with anything and just leave the scheme as it is. You can apply this to controllers which return dynamic resources embedded in another HTTP or HTTPS page (like CSS and images). So if the requesting page is using HTTPS, the resource controller can apply [RetainHttps] and say “just keep doing whatever you were doing”. Here it is:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RetainHttpsAttribute : Attribute {
}
Benefits
It’s reasonably DRY - you can apply the [RetainHttps] [ExitHttpsIfNotRequired] attribute to your base controller class once, and then selectively apply [RequireHttps] and [RetainHttps] where required.
Also, it’s relatively simple to understand and implement.
Shortcomings
Entering and exiting HTTPS is always done with a redirect. Links which are entrance or exit points to HTTPS are shown in the incorrect scheme, and only by following the incorrect link do you end up at the correct one. Not ideal, but it’s not too bad.
Overall
I think the relative simplicity of this approach makes it a very good option. I’m also confident enough to use it in a production. I’d be very interested to hear whether or not you think it’s a good approach.
Thanks for reading!