Wednesday, 7 September 2011

RequireHttps attribute in MVC3

I came across this gem of an attribute when looking to secure one of my controllers in a asp.net MVC 3 project.
[RequireHttps(Order = 1)]
public class PayController : Controller {
   ...
}
This will automatically redirect and any url:-

from: http://www.mydomain.com/pay/membership

to: https://www.mydomain.com/pay/membership

However this comes with one small glitch, after the user has finished with the pay controller and then navigates to say the home controller then the browser still shows https:// in the address bar. So is there a elegant solution to make all your other controllers http only?

One way is to create a base controller that overrides the OnAuthorization method
public class BaseController : Controller {
  protected override void OnAuthorization(
        AuthorizationContext filterContext) {

    //Only check if we are already on a secure connectuion and 
    // we don't have a [RequireHttpsAttribute] defined
    if (Request.IsSecureConnection) {
      var requireHttps = filterContext.ActionDescriptor
                  .GetCustomAttributes(
                     typeof(RequireHttpsAttribute), false)
                  .Count() >= 1;

      //If we don't need SSL and we are not on a child action
      if (!requireHttps && !filterContext.IsChildAction) {
        var uriBuilder = new UriBuilder(Request.Url) {
                                 Scheme = "http", 
                                 Port = 80
                                };
        filterContext.Result = 
             this.Redirect(uriBuilder.Uri.AbsoluteUri);
      }
    }
    base.OnAuthorization(filterContext);
  }
}
And then make all our non secure controllers inherit from this base controller:
public class CmsHomeController : BaseController {
  ...
}
Lovely!

5 comments:

  1. A bit of caution here, if the user has any sort of authentication token and you revert back to non-ssl, be careful as it could easily be stolen at that point. Just an fyi for anyone else that reads this. thanks for the posting btw!

    ReplyDelete
  2. Brilliant. I have read a few other workarounds but this is the simplest and most elegant solution. Besides, it is a common practice and a good habit to inherit from a BaseController anyway.

    Thanks a bunch for sharing this. Rock on, Rippo.

    BTW, your captcha is almost human-proof... :) It took me a few tries.

    -- LK, from Florida, US.

    ReplyDelete
  3. PS: this solution does not work when [RequireHttps] attribute is declared at the Controller class level, although that is a relatively small issue to deal with.

    -- LK

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
  4. Update: I have posted a modified version on overstack.com that handles the request at both Controller class and action levels and utilize any port that IIS Express assign.

    http://stackoverflow.com/questions/1639707/asp-net-mvc-requirehttps-in-production-only/12166339#12166339
    (Look for Keng)

    Here is the code in BaseController class

    #region Override to reroute to non-SSL port if controller action does not have RequireHttps attribute to save on CPU
    // Note that this code works with RequireHttps at the controller class or action level.
    // Credit: Various stackoverflow.com posts and http://puredotnetcoder.blogspot.com/2011/09/requirehttps-attribute-in-mvc3.html
    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
    // if the controller class or the action has RequireHttps attribute
    var requireHttps = (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Count() > 0
    || filterContext.ActionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true).Count() > 0);
    if (Request.IsSecureConnection)
    {
    // If request has a secure connection but we don't need SSL, and we are not on a child action
    if (!requireHttps && !filterContext.IsChildAction)
    {
    var uriBuilder = new UriBuilder(Request.Url)
    {
    Scheme = "http",
    Port = int.Parse(getConfig("HttpPort", "80")) // grab from config; default to port 80
    };
    filterContext.Result = this.Redirect(uriBuilder.Uri.AbsoluteUri);
    }
    }
    else
    {
    // If request does not have a secure connection but we need SSL, and we are not on a child action
    if (requireHttps && !filterContext.IsChildAction)
    {
    var uriBuilder = new UriBuilder(Request.Url)
    {
    Scheme = "https",
    Port = int.Parse(getConfig("HttpsPort", "443")) // grab from config; default to port 443
    };
    filterContext.Result = this.Redirect(uriBuilder.Uri.AbsoluteUri);
    }
    }
    base.OnAuthorization(filterContext);
    }
    #endregion

    // a useful helper function to get appSettings value; allow caller to specify a default value if one cannot be found
    internal static string getConfig(string name, string defaultValue = null)
    {
    var val = System.Configuration.ConfigurationManager.AppSettings[name];
    return (val == null ? defaultValue : val);
    }
    Thanks.

    ReplyDelete