Tuesday, October 25, 2011

RequireHttps attribute and IIS Express

Perhaps you, like me, have been frustrated by the RequireHttpsAttribute functionality in MVC.  It's a great concept; an easy attribute class to force redirection to a secure channel, but it was kinda frustrating how it was implemented, especially since the advent of IIS Express.

If you didn't know, IIS Express is a great tool that integrates seamlessly into Visual Studio 2010.  It works very well, and you can enable HTTP over SSL with the click of a mouse.  I love it; there's only one problem: it assigns both the cleartext and the secure channel to unique TCP ports.  From IIS Express's perspective, this is a good thing, keeping everything nice and separate... from a development perspective, this is annoying, because we do not have the ability to tell our code which port to use when redirecting to the secure channel.

Well this cheesed me off enough that I decided to do something about it.  I liked the idea of an attribute class controlling the redirect; it kept my controllers clean from clutter; however, the IIS secure port is machine specific, and in production it's almost always going to be 443.  Attributes do not support variable arguments, so I had to get a little creative.  What I ended up with is this:


using System.Configuration;

namespace System.Web.Mvc
{
[AttributeUsage(AttributeTargets.Class)]
public class RedirectHttpsAttribute : ActionFilterAttribute
{
#region Properties
private static string _DisableHttpsRedirect;
/// <summary>
/// When true, forcing a redirect to a secure page is suppressed.
/// </summary>
/// <remarks>
/// This value can be set in the config file under the "DisableHttpsRedirect" app setting.
/// </remarks>
public static bool DisableHttpsRedirect
{
get
{
if (string.IsNullOrEmpty(_DisableHttpsRedirect))
{
_DisableHttpsRedirect = (ConfigurationManager.AppSettings["DisableHttpsRedirect"] ?? "False");
}

bool retValue;
if (!bool.TryParse(_DisableHttpsRedirect, out retValue))
{
retValue = false;
}

return retValue;
}

set
{
_DisableHttpsRedirect = value.ToString();
}

}

private static string _SecurePort;
/// <summary>
/// When set to a number, explicitly declares the port of the URL in the redirect string.
/// When set to -1, uses the default protocol port (no port number in the string)
/// </summary>
/// <remarks>
/// This value can be set in the config file under the "SecurePort" app setting.
/// </remarks>
public static int SecurePort
{
get
{
if (string.IsNullOrEmpty(_SecurePort))
{
_SecurePort = (ConfigurationManager.AppSettings["SecurePort"] ?? "-1");
}

int retValue;
if (!int.TryParse(_SecurePort, out retValue))
{
retValue = -1;
}

return retValue;
}

set
{
_SecurePort = value.ToString();
}
}
#endregion

public override void OnResultExecuting(ResultExecutingContext filterContext)
{

//Allow the redirect to be disabled in the config file.  This may be necessary for testing purposes.
if (!filterContext.HttpContext.Request.IsSecureConnection &&
!DisableHttpsRedirect)
{
//replace the contents of the nonsecure string with the proper protocol and port number.
UriBuilder ub = new UriBuilder(filterContext.HttpContext.Request.Url);
ub.Scheme = "https";
ub.Port = SecurePort;

//direct the context to redirect to the proper secure channel.
filterContext.Result = new RedirectResult(ub.ToString());
filterContext.Result.ExecuteResult(filterContext);
}

return;
}
}
}

This allows the HTTPS port to be configured in the web.config file as follows:

  • Setting: "DisableHttpsRedirect" this can be used on a dev machine with no access to SSL; shouldn't be necessary in most cases with IIS express, but it's possible.  The default value is "false".  Whenever you're forcing control of an application somewhere else, it's a good idea to expose a means to override the behavior for isolation purposes.
  • Setting: "SecurePort" this allows you to set the secure port number in your web.config file.  The default is -1 which the URL builder will take to mean the default port (in the case of HTTPS, that is 443)  This way, you can have your production environment run without a port number specifier, which would look goofy.

There!  All we have to do now is make an abstract Controller class tagged with the [RedirectHttps] attribute, have the rest of our controllers inherit from it and configure the joker in the web config.  Problem solved.

No comments:

Post a Comment