ASP.NET MVC RequireHttps in Production Only
Asked Answered
H

16

122

I want to use the RequireHttpsAttribute to prevent unsecured HTTP requests from being sent to an action method.

C#

[RequireHttps] //apply to all actions in controller
public class SomeController 
{
    [RequireHttps] //apply to this action only
    public ActionResult SomeAction()
    {
        ...
    }
}

VB

<RequireHttps()> _
Public Class SomeController

    <RequireHttps()> _
    Public Function SomeAction() As ActionResult
        ...
    End Function

End Class

Unfortunately, ASP.NET Development Server doesn't support HTTPS.

How can I make my ASP.NET MVC application use RequireHttps when published to the production environment, but not when run on my development workstation on the ASP.NET Development Server?

Hrutkay answered 28/10, 2009 at 19:57 Comment(1)
Test with your local IIS and with IIS Express. See my SSL blog blogs.msdn.com/b/rickandy/archive/2011/04/22/… and blogs.msdn.com/b/rickandy/archive/2012/03/23/…Bobbitt
A
130

This won't help if you run Release builds on your development workstation, but conditional compilation could do the job...

#if !DEBUG
[RequireHttps] //apply to all actions in controller
#endif
public class SomeController 
{
    //... or ...
#if !DEBUG
    [RequireHttps] //apply to this action only
#endif
    public ActionResult SomeAction()
    {
    }

}

Update

In Visual Basic, attributes are technically part of the same line as the definition they apply to. You can't put conditional compilation statements inside a line, so you're forced to write the function declaration twice - once with the attribute, and once without. It does work, though, if you don't mind the ugliness.

#If Not Debug Then
    <RequireHttps()> _
    Function SomeAction() As ActionResult
#Else
    Function SomeAction() As ActionResult
#End If
        ...
    End Function

Update 2

Several people have mentioned deriving from RequireHttpsAttribute without providing an example, so here's one for you. I think that this approach would be much cleaner than the conditional compilation approach, and it would be my preference in your position.

DISCLAIMER: I haven't tested this code, even a little bit, and my VB is fairly rusty. All I know is that it compiles. I wrote it based on the suggestions of spot, queen3, and Lance Fisher. If it doesn't work, it should at least convey the general idea, and give you starting point.

Public Class RemoteRequireHttpsAttribute
    Inherits System.Web.Mvc.RequireHttpsAttribute

    Public Overrides Sub OnAuthorization(ByVal filterContext As  _
                                         System.Web.Mvc.AuthorizationContext)
        If IsNothing(filterContext) Then
            Throw New ArgumentNullException("filterContext")
        End If

        If Not IsNothing(filterContext.HttpContext) AndAlso _
            filterContext.HttpContext.Request.IsLocal Then
            Return
        End If

        MyBase.OnAuthorization(filterContext)
    End Sub

End Class

Basically, the new attribute just quits out instead of running the default SSL authorization code, if the current request is local (that is, you're accessing the site through localhost). You can use it like this:

<RemoteRequireHttps()> _
Public Class SomeController

    <RemoteRequireHttps()> _
    Public Function SomeAction() As ActionResult
        ...
    End Function

End Class

Much cleaner! Provided my un-tested code actually works.

Abaddon answered 28/10, 2009 at 20:13 Comment(6)
Thanks for, um, editing my post for me, Zack. Your question was in C#, so I posted a C# response. I didn't know VB was relevant. Anyone know if there is a way to use conditional compilation to control attributes in VB, or is that just not possible?Abaddon
Yes, it works for C#, and it works for VB too, but you have to do some rather ugly duplication of the function/class definition. See my updated answer above.Abaddon
Sorry. VB code samples are getting harder and harder to come by. I didn't think it would matter. I've updated the original question. Do conditional compilation around attributes work for sure in C#? I haven't tested. That seems like a perfect, elegant solution.Hrutkay
Your RemoteRequireHttpsAttribute code works perfectly. That's much more elegant for VB than the conditional compilation. Thanks again Joel.Hrutkay
how can filterContext ever be null? is this just your defensive programming standards - or is there ever a reason?Rorqual
That's me not knowing much about the context in which this code might be invoked, so I thought I'd play it safe. Feel free to leave the null check out...Abaddon
F
65

If anyone needs the C# version:

using System;
using System.Web.Mvc;

namespace My.Utils
{
    public class MyRequireHttpsAttribute : RequireHttpsAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }

            if (filterContext.HttpContext != null && filterContext.HttpContext.Request.IsLocal)
            {
                return;
            }

            base.OnAuthorization(filterContext);
        }
    }
}
Foamy answered 3/5, 2011 at 15:45 Comment(2)
ok when reading this and this as a security measure should we add filters.Add(new MyRequireHttpsAttribute ()); in FilterConfig ?Circumlocution
Based on this answer I created a solution for MVC 6 using either filter in Startup.cs or attribute style on Controller.Jacintajacinth
A
26

Deriving from RequireHttps is a good approach.

To side step the issue entirely, you can use IIS on your local machine with a self-signed certificate too. IIS is faster than the built-in webserver, and you have the advantage that your development environment is more like production.

Scott Hanselman has a great resource on a few ways to implement local HTTPS with VS2010 and IIS Express.

Allhallowtide answered 28/10, 2009 at 21:8 Comment(5)
ya - until you try to do port forwarding with a Mifi wifi Verizon device and find that port 443 is not available to forward!!! #*&#*&$Rorqual
What I don't like about using IIS on your local machine with a self-signed certificate is that I have to go through an extra step of deployment to test changes. I think that if you are testing something related to security than it makes sense, but say if you are just checking some other minor change, it is a pain to have to deploy just to get around Cassini's inability to support HTTPS.Agar
@Agar - Use IIS express on client versions of windows, no cassini needed and it will work exactly like IIS, including having ssl capability.Pennon
@Mystere Man - yeah, I did find that out since that comment. Thanks for the tip :)Agar
more information or link should be added on how to go about doing such things.Peterman
D
12

Leveraging the MVC filter system and Global.asax.cs, I'm assuming you could do this...

    protected void Application_Start()
    {
      RegisterGlobalFilters(GlobalFilters.Filters);
    }

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
      filters.Add(new HandleErrorAttribute());
      if(Config.IsProduction) //Some flag that you can tell if you are in your production environment.
      {
        filters.Add(new RequireHttpsAttribute());
      }
    }
Danford answered 15/9, 2011 at 20:12 Comment(1)
I prefer this answer as it involves one check per application lifetime rather than implementing a new filter that's going to be executed\called with every single request.Lanate
B
10

As it was the ASP.Net Development Server that caused your problem in the first place, it's worth noting that Microsoft now has IIS Express, which ships with Visual Studio (since VS2010 SP1). This is a cut-down version of IIS that is as easy to use as the Development Server, but supports the full feature set of IIS 7.5 including SSL.

Scott Hanselman has a detailed post on working with SSL in IIS Express.

Bolduc answered 13/7, 2010 at 10:25 Comment(0)
B
9

How about inheriting the RequireHttps attribute in a custom attribute. Then, inside your custom attribute, check the IsLocal property of the current request to see if the request is coming from the local machine. If it is, then do not apply the base functionality. Otherwise, call the base operation.

Boggess answered 28/10, 2009 at 20:8 Comment(0)
D
4

This worked for me, MVC 6 (ASP.NET Core 1.0). Code checks if debug is in development, and if not, ssl is not required. All edits are in Startup.cs.

Add:

private IHostingEnvironment CurrentEnvironment { get; set; }

Add:

public Startup(IHostingEnvironment env)
{
    CurrentEnvironment = env;
}

Edit:

public void ConfigureServices(IServiceCollection services)
{
    // additional services...

    services.AddMvc(options =>
    {
        if (!CurrentEnvironment.IsDevelopment())
        {
            options.Filters.Add(typeof(RequireHttpsAttribute));
        }
    });
}
Diarrhea answered 19/9, 2016 at 7:51 Comment(0)
C
3

If you can derive and override - do it. If you can't - MVC comes with sources, just take the sources and create your own [ForceHttps] attribute that checks IsLocal.

Catalepsy answered 28/10, 2009 at 20:56 Comment(0)
L
3

As Joel mentioned you can alter the compilation by using the #if !DEBUG directive.

I just found out that you can alter the value of the DEBUG symbol in the web.config file compilation element. Hope that helps.

Languish answered 3/11, 2010 at 13:8 Comment(0)
N
3

For MVC 3 I added my own FilterProvider (based on code found here: Global and Conditional Filters that, among other things (displaying Debug info for local users etc.) will decorate all actions with RequireHttpsAttribute when HttpContext.Request.IsLocal == false.

Nissen answered 30/3, 2011 at 8:56 Comment(1)
Or you can just conditionally add it to the global filter collection when the request is local. Note that you'll want to check this in a try/catch block if the app is set to start up immediately since the request may not be available.Splendent
A
3

After researching aroud, I was able to solve this issue with IIS Express and an override of the Controller class's OnAuthorization method (Ref#1). I have also gone with the route recommended by Hanselman (Ref#2). However, I was not complete satisfied with these two solutions due to two reasons: 1. Ref#1's OnAuthorization only works at the action level, not at the controller class level 2. Ref#2 requires a lot of setup (Win7 SDK for makecert), netsh commands, and, in order to use port 80 and port 443, I need to launch VS2010 as administrator, which I frown upon.

So, I came up with this solution that focuses on simplicity with the following conditions:

  1. I want to be able to use the RequireHttps attbbute at Controller class or action level

  2. I want MVC to use HTTPS when the RequireHttps attribute is present, and use HTTP if it is absent

  3. I do not want to have to run Visual Studio as administrator

  4. I want to be able to use any HTTP and HTTPS ports that are assigned by IIS Express (See Note#1)

  5. I can reuse the self-signed SSL cert of IIS Express, and I do not care if I see the invalid SSL prompt

  6. I want dev, test, and production to have the exact same code base and same binary and as independent from additional setup (e.g. using netsh, mmc cert snap-in, etc.) as possible

Now, with the background and explanation out of the way, I hope this code will help someone and save some time. Basically, create a BaseController class that inherits from Controller, and derive your controller classes from this base class. Since you have read this far, I assume that you know how to do these. So, happy coding!

Note#1: This is achieved by the use of a useful function 'getConfig' (see code)

Ref#1: http://puredotnetcoder.blogspot.com/2011/09/requirehttps-attribute-in-mvc3.html

Ref#2: http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx

========== Code in BaseController ===================

     #region Override to reroute to non-SSL port if controller action does not have RequireHttps attribute to save on CPU 
    // By L. Keng, 2012/08/27
    // 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);
    }

============== end code ================

In Web.Release.Config, add the following to clear out HttpPort and HttpsPort (to use the default 80 and 443).

<appSettings>
<add key="HttpPort" value="" xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
<add key="HttpsPort" value="" xdt:Transform="SetAttributes" xdt:Locator="Match(key)"/>
</appSettings>
Algy answered 28/8, 2012 at 19:38 Comment(0)
H
3

One solution you can use on production as well as on development workstation. It's based on your option from application settings in web.config

<appSettings>
     <!--Use SSL port 44300 in IIS Express on development workstation-->
     <add key="UseSSL" value="44300" />
</appSettings>

If you don't want to use SSL remove the key. If you use standard SSL port 443, then remove the value or specify 443.

Then use custom implementation of RequireHttpsAttribute that takes care of your condition. It is derived actually from RequireHttps and uses the same implementation of the base method except for adding conditions.

public class RequireHttpsConditional : RequireHttpsAttribute
{
    protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
    {
        var useSslConfig = ConfigurationManager.AppSettings["UseSSL"];
        if (useSslConfig != null)
        {
            if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException("The requested resource can only be accessed via SSL.");
            }

            var request = filterContext.HttpContext.Request;
            string url = null;
            int sslPort;

            if (Int32.TryParse(useSslConfig, out sslPort) && sslPort > 0)
            {
                url = "https://" + request.Url.Host + request.RawUrl;

                if (sslPort != 443)
                {
                    var builder = new UriBuilder(url) {Port = sslPort};
                    url = builder.Uri.ToString();
                }
            }

            if (sslPort != request.Url.Port)
            {
                filterContext.Result = new RedirectResult(url);
            }
        }
    }
}

Don't forget to decorate LogOn method in AccountController

[RequireHttpsConditional]
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)

and something like this in your LogOn View in order to post form over https.

<% using (Html.BeginFormSecure("LogOn", "Account", new { ReturnUrl = Request.QueryString["ReturnUrl"] }, Request.IsSecureConnection, Request.Url)) { %>
Hellenism answered 21/7, 2013 at 18:19 Comment(1)
i am getting this error: XMLHttpRequest cannot load m.XXX.com/Auth/SignIn. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'm.XXX.com' is therefore not allowed access.Liquefacient
J
1

MVC 6 (ASP.NET Core 1.0):

The proper solution would be to use env.IsProduction() or env.IsDevelopment(). Read more about reason behind in this answer on how to require https only in production.

Condensed answer below (see link above to read more about design decisions) for 2 different styles:

  1. Startup.cs - register filter
  2. BaseController - attribute style

Startup.cs (register filter):

public void ConfigureServices(IServiceCollection services)
{
    // TODO: Register other services

    services.AddMvc(options =>
    {
        options.Filters.Add(typeof(RequireHttpsInProductionAttribute));
    });
}

BaseController.cs (attribute style):

[RequireHttpsInProductionAttribute]
public class BaseController : Controller
{
    // Maybe you have other shared controller logic..
}

public class HomeController : BaseController
{
    // Add endpoints (GET / POST) for Home controller
}

RequireHttpsInProductionAttribute: Both of above are using custom attribute inheriting from RequireHttpsAttribute:

public class RequireHttpsInProductionAttribute : RequireHttpsAttribute
{
    private bool IsProduction { get; }

    public RequireHttpsInProductionAttribute(IHostingEnvironment environment)
    {
        if (environment == null)
            throw new ArgumentNullException(nameof(environment));
        this.IsProduction = environment.IsProduction(); 
    }
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (this.IsProduction)
            base.OnAuthorization(filterContext);
    }
    protected override void HandleNonHttpsRequest(AuthorizationContext filterContext)
    {
        if(this.IsProduction)
            base.HandleNonHttpsRequest(filterContext);
    }
}
Jacintajacinth answered 7/7, 2016 at 12:34 Comment(0)
V
1

This was the cleanest way for me. In my App_Start\FilterConfig.cs file. Can't run release builds anymore though.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (!Web.HttpContext.Current.IsDebuggingEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}

Alternatively, you could set it to only require https when your custom error page is on.

... 
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
        if (Web.HttpContext.Current.IsCustomErrorEnabled) {
            filters.Add(new RequireHttpsAttribute());   
        }
        ...
}
Vedis answered 3/11, 2016 at 22:37 Comment(1)
This is a easy solution that works great in MVC 5 :)Gnosticism
S
0

You can set in global.asax to use SSL only. Then use #if !DEBUG to avoid using RequireHttpsAttribute() it on local

public class MvcApplication : System.Web.HttpApplication
    { 
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas(); 
  WebApiConfig.Register(System.Web.Http.GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
    #if !DEBUG
            GlobalFilters.Filters.Add(new RequireHttpsAttribute());
     #endif
        }
}
Sweatband answered 2/9, 2022 at 7:10 Comment(0)
D
-1

Please refer to this post by Rick Anderson on RickAndMSFT on Azure & MVC Filling the Azure Gap

http://blogs.msdn.com/b/rickandy/archive/2011/04/22/better-faster-easier-ssl-testing-for-asp-net-mvc-amp-webforms.aspx

Dunkirk answered 1/9, 2014 at 9:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.