8

So I've found the [RequiresHttps] attribute but once your in https your kind of stuck there, so to try and be able to have actions on a single url (and scheme) I've found I've ended up having to create my own ExtendedController to revert back to http for actions that don't use [RequireHttps].

Just wondering if what I'm doing is okay or if there is a better way?

public class ExtendedController : Controller
{
    protected virtual void HandleHttpRequest(AuthorizationContext filterContext)
    {
        if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("Cannot post between https and http.");
        }
        string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
        filterContext.Result = new RedirectResult(url);
    }

    protected override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);
        object[] attributes = filterContext.ActionDescriptor.GetCustomAttributes(true);
        if (!attributes.Any(a => a is RequireHttpsAttribute))
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }
            if (filterContext.HttpContext.Request.IsSecureConnection)
            {
                this.HandleHttpRequest(filterContext);
            }
        }
    }
}
Mark
  • 880
  • 8
  • 18
  • One thing I have just realised is that I should also check on filterContext.IsChildAction - I wonder if there is potential to run into the same issues using RequiresHttp on child actions. Seems like a more complete solution is needed perhaps on routing side instead of on controllers. – Mark Nov 25 '10 at 14:05

2 Answers2

11

What you have is syntatically correct, however a suggestion is to create a new Action filter that inherits from the default RequireHttpsAttribute and takes a parameter to switch between http and https.

public class RequireHttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
    public bool RequireSecure = false;

    public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)

    {
        if (RequireSecure)
        {
            base.OnAuthorization(filterContext);
        }
        else
        {
            // non secure requested
            if (filterContext.HttpContext.Request.IsSecureConnection)
            {
                HandleNonHttpRequest(filterContext);
            }
        }
    }

    protected virtual void HandleNonHttpRequest(AuthorizationContext filterContext)
    {
        if (String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
        {
            // redirect to HTTP version of page
            string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url);
        }
    } 
}

Then, on your action method or controller you would use:

[RequireHttps (RequireSecure = true)]

...

or

[RequireHttps (RequireSecure = false)]
ataravati
  • 8,891
  • 9
  • 57
  • 89
Clicktricity
  • 4,171
  • 1
  • 23
  • 15
  • One thing to note is this defaults to non-secure "public bool RequireSecure = false;" whereas the System.Web.Mvc.RequireHttpsAttribute requires HTTPS. If you want to parallel the System.Web.Mvc.RequireHttpsAttribute just set the default to "true". – ChrisP Feb 19 '14 at 22:26
1

To make it little more manageable. This solution assumes that majority of your web application use HTTP scheme.

  1. Create new action filter RequiresHttp (use HTTP if NeedSsl attribute is not apply explicitly on action or controller),

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        HttpRequestBase req = filterContext.HttpContext.Request;
        HttpResponseBase res = filterContext.HttpContext.Response;
    
        bool needSsl = filterContext.ActionDescriptor.IsDefined(typeof(NeedSslAttribute), true)
                        || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(NeedSslAttribute), true);
    
    
        if (needSsl && !req.IsSecureConnection) //https: secure
        {
            var builder = new UriBuilder(req.Url)
            {
                Scheme = Uri.UriSchemeHttps,
                Port = 444
            };
            res.Redirect(builder.Uri.ToString());
        }
        else if (!needSsl && req.IsSecureConnection) //http: non secure
        {
            var builder = new UriBuilder(req.Url)
            {
                Scheme = Uri.UriSchemeHttp,
                Port = 8081
            };
            res.Redirect(builder.Uri.ToString());
        }
        base.OnActionExecuting(filterContext);
    }
    
  2. And new blank attribute NeedSSL (for indication purpose)

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class NeedSslAttribute : Attribute { }
    
  3. Apply RequiresHttp as global action filter in Global.aspx.cs

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new RequiresHttp());
    }
    
  4. Now apply apply NeedSslAttribute on controllers and actions where do you want to use HTTPS scheme

    [NeedSsl]
    [AllowAnonymous]
    public ActionResult LogOn()
    

This code is not perfect as action filter RequiresHttp does multiple jobs i.e. check NeedSsl attribute and apply HTTP or HTTPS scheme. Would have been better if we could use two action filters RequiresHTTP and RequiresHTTPS.

Now if RequiresHTTP was set as global filter and RequiresHTTPS filter was applied on specific actions and specific RequiresHTTPS filter would have given preference.

eckes
  • 64,417
  • 29
  • 168
  • 201
Arvind Dhasmana
  • 1,396
  • 2
  • 9
  • 8