I want to use the RequireHttpsAttribute to prevent unsecured HTTP requests from being sent to an action method.
C#
[RequireHttps] //apply to all acti
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.
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)) { %>
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()
{
}
}
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
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.
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.
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);
}
}
}
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());
}
}