Introduction

Securing pages of a web application is an important step for many production-ready C# ASP .NET MVC web applications, especially those dealing with security-sensitive or personal data, such as passwords or financial information. The primary method for securing web pages is through the use of a secure sockets layer (SSL) on the HTTPS protocol. Since SSL invokes additional encryption/decryption and processing routines, it can require additional resources and load on the web server CPU. For this reason, C# ASP .NET MVC web application developers may choose to protect specific key pages with HTTPS SSL, rather than enabling secure sockets on the entire site.

In this tutorial, we’ll walk through the steps of enabling HTTPS SSL security on specific pages on a C# ASP .NET MVC3 web application. We’ll implement a basic user-login application that consists of a home page (HTTP), a login page (HTTPS), a logged-in menu page (HTTP), and a secure “change-password” modal dialog (HTTPS). Our demo will demonstrate transitioning from the non-secure HTTP home page to the secure HTTPS login page; transitioning back to the non-secure logged-in menu page; and finally the complexity involved with posting data from a non-secure page to a secure modal dialog via cross-domain functionality through the use of JSONP. Our application can be configured with a single web.config parameter to enable or disable the HTTP SSL functionality (especially useful during development). To make the example more interesting, our C# ASP .NET MVC web site will be a haunting application, allowing a ghost to login to the system to select a target victim to haunt!

Layout of the Ethereal

Before diving into the local setup for SSL HTTPS and the implementation details, let’s briefly take a look at the layout journey for ghosts logging into our web application.

Entrance to the Graveyard (HTTP) -> Gateway (HTTPS) -> Graveyard (HTTP) <-> Haunt Popup (HTTPS)

Or in traditional terms

Home (HTTP) -> Login (HTTPS) -> Main Menu (HTTP) <-> Change Password Popup (HTTPS)

A ghost’s typical journey includes accessing the non-secure, public homepage for our web application. By utilizing plain HTTP for the home page, we retain minimal web server CPU impact and optimal SEO capability. Ghosts wishing to login can click the Enter link to access the Gateway page. Since the ghost will be entering their username and skeleton key, we’ll need to secure this page with SSL HTTPS, securing the sensitive user login details (password) during transmission from the web browser client to the web server.

After moving through the gateway, the ghost reaches the main menu. To conserve web server processing, we transition back to plain HTTP. The ghost may then choose to haunt a victim. Since the ghost must ensure his anonymity and not let his victim’s name and length of the haunting to be known, we protect the Haunt modal popup dialog with SSL HTTPS. In this case, we’ll be posting the form data from a non-secure HTTP page to a secure HTTPS controller. Since we’ll be moving cross-domain (HTTP -> HTTPS) in an AJAX modal dialog, we’ll need to utilize JSONP to complete the request.

Also note, the ghost is redirected to plain HTTP for any web page by default, unless we explicitly declare a page or method to be HTTP SSL secure.

The Making of a Ghost

We’ll start with a very basic model for our ghost. A similar model may be used for the traditional “User” in a web application.

1 2 3 4 5 6 7 8 public class Ghost { [Required] public string Name { get ; set ; } [Required, DataType(DataType.Password)] public string SkeletonKey { get ; set ; } }

Since the whole point of our web application is to allow a ghost to secretly haunt a victim, we’ll need a model for that too:

1 2 3 4 5 6 7 8 public class HauntTarget { [Required] public string HumanName { get ; set ; } [Required] public int DaysToHaunt { get ; set ; } }

Simple enough. Note, the SkeletonKey property is just like a password. We’ll receive this data in a secure SSL HTTPS form post from the Gateway page to our C# ASP .NET MVC controller.

The Nitty Gritty

We’ll use a couple of C# ASP .NET MVC attributes to make our lives easier. Since our web application allows users to login to a system with protected pages, the first attribute is the AllowAnonymousAttribute. Any MVC controller method that is decorated with this attribute will allow anonymous ghosts to view the page. All other pages and C# MVC3 controller methods will be assumed to be protected behind a login. This is the alternative to using the web.config to define login-protected pages.

1 2 3 4 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false , Inherited = true )] public sealed class AllowAnonymousAttribute : Attribute { }

We combine this with a custom filter for performing logon authorization:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public sealed class LogonAuthorize : AuthorizeAttribute { public override void OnAuthorization ( AuthorizationContext filterContext ) { bool skipAuthorization = filterContext.ActionDescriptor.IsDefined( typeof (AllowAnonymousAttribute), true ) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined( typeof (AllowAnonymousAttribute), true ); if (!skipAuthorization) { base .OnAuthorization(filterContext); } } }

The above can be wired into the C# ASP .NET MVC application through the Global.asax.cs:

1 2 3 4 5 public static void RegisterGlobalFilters (GlobalFilterCollection filters) { filters.Add( new HandleErrorAttribute()); filters.Add( new LogonAuthorize()); }

Adding a Kill Switch to RequireHttpsAttribute

Our next attribute is an override for the traditional C# ASP .NET MVC RequireHttpsAttribute. Normally, decorating a controller method with RequireHttpsAttribute will always enforce the page to load under SSL HTTPS. Since we want to make our developer lives easier, we’ll create a web.config boolean flag to allow enabling or disabling SSL HTTPS across the site.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class RequireHttpsByConfig : RequireHttpsAttribute { public override void OnAuthorization ( AuthorizationContext filterContext ) { if (ConfigurationManager.AppSettings[ "EnableSSL" ].ToLower() != "true" ) { return ; } else { base .OnAuthorization(filterContext); } } }

Our new RequireHttpsByConfig first reads the web.config value for EnableSSL. If the value is not true, we cancel the RequireHttpsAttribute logic. Otherwise, we allow it to process the request.

Bare Bones By Default

Since our C# ASP .NET MVC web application will use non-secure HTTP by default for web pages (unless explicitly defined as an HTTPS SSL page), we’ll need the following MVC attribute (originally from Entering and Exiting HTTPS with ASP .NET MVC):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class ExitHttpsIfNotRequiredAttribute : FilterAttribute , IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { if (!filterContext.HttpContext.Request.IsSecureConnection) return ; if (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true ).Length > 0 ) return ; if (filterContext.ActionDescriptor.GetCustomAttributes(typeof(RequireHttpsAttribute), true ).Length > 0 ) return ; if (filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true ).Length > 0 ) return ; if (filterContext.ActionDescriptor.GetCustomAttributes(typeof(RetainHttpsAttribute), true ).Length > 0 ) return ; if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET" , StringComparison.OrdinalIgnoreCase)) return ; if (filterContext.IsChildAction) return ; string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl; filterContext.Result = new RedirectResult(url); } }

By decorating all of our controllers with the ExitHttpsIfNotRequiredAttribute, we can ensure all pages will transition from HTTPS to HTTP, by default. Rather than paste the attribute on every controller we make, we can create a simple MVC base controller, decorated with the attribute, and inherit all MVC page controllers from it.

1 2 3 4 [ExitHttpsIfNotRequiredAttribute] public abstract class BaseController : Controller { }

The Graveyard Entrance

Our home page should be public for all to see. Since SSL HTTPS security is not required here, we’ll leave out defining the RequireHttpsByConfig attribute. We’ll also decorate our controller’s Index method to allow anonymous ghosts (non-logged in ghosts).

1 2 3 4 5 6 7 8 public class HomeController : BaseController { [AllowAnonymous] public ActionResult Index() { return View(); } }

The Gateway Controller

Things get more interesting when ghosts enter the gateway. They’ll be required to provide their username and skeleton key in order to enter the realm. Since this data is sensitive, just like a username and password, we’ll explicitly require HTTPS SSL on this page (and all methods of the controller).

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class GatewayController : BaseController { [RequireHttpsByConfig, AllowAnonymous] public ActionResult Index ( ) { return View( new Ghost()); } [RequireHttpsByConfig, AllowAnonymous, HttpPost] public ActionResult Index ( Ghost user, string returnUrl ) { if (ModelState.IsValid) { if (Membership.ValidateUser(user.Name, user.SkeletonKey)) { FormsAuthentication.SetAuthCookie(user.Name, false ); if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith( "/" ) && !returnUrl.StartsWith( "//" ) && !returnUrl.StartsWith( "/\\" )) { return Redirect(returnUrl); } else { return RedirectToAction( "Index" , "graveyard" ); } } else { ModelState.AddModelError( "" , "You must supply the correct skeleton key to enter the graveyard." ); } } return View(user); } }

Notice in the above code, we’ve defined the login page, itself, as HTTPS SSL secure. This lets the user know his information will be protected before even posting the form data. We’ve also defined the postback for the form as HTTPS SSL secure. Of course, both methods also allow anonymous ghosts, since they have not yet been authenticated.

A Touch of Magic in Transitioning from HTTPS to HTTP

After authenticating through the gateway, our ghost reaches the graveyard. The magic of the ExitHttpsIfNotRequiredAttribute takes effect when navigating from the HTTPS SSL gateway login page to the HTTP graveyard main menu. Our graveyard controller is only accessible to authenticated ghosts, and therefore, does not define the AllowAnonymous attribute. It also does not define the RequireHttpsByConfig attribute, since it is available in plain HTTP.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class GraveyardController : BaseController { public ActionResult Index ( ) { return View(); } public ActionResult Exorcise ( ) { HttpRuntime.Cache.Remove(User.Identity.Name); FormsAuthentication.SignOut(); return RedirectToAction( "Index" , "Home" ); } }

Our ghost is going to want to haunt a target victim. Due to the secrecy involved, he’ll want to ensure his victim’s name and length of the haunting is secure in transmission from client to server. However, the AJAX modal popup that contains the haunt form is hosted on an unprotected HTTP main menu page. We can ensure encryption of the information by posting the form data to a secure SSL HTTPS MVC controller method. However, moving from HTTP to HTTPS with AJAX is a cross-domain policy violation. The protocol considers a move from HTTP to HTTPS to be the same as a move from one domain to a completely different one (ie., www.mysite.com -> www.somethingelse.com). Therefore, we’ll be required to post with JSONP, which in turn, requires us to use an HTTP GET request. Our final controller method will appear as follows:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class GhostController : Controller { [RequireHttpsByConfig, JsonpFilter] public JsonResult Haunt ( HauntTarget hauntTarget ) { string status = "" ; if (ModelState.IsValid) { if (hauntTarget.DaysToHaunt > 3 ) { status = "You're not powerful enough yet. Try less days." ; } else { status = "OK" ; } } return Json( new { Status = status, Name = hauntTarget.HumanName, Days = hauntTarget.DaysToHaunt }); } }

Note in the above code, we allow both GET and POST requests. This allows us to still use the method when disabling SSL HTTPS via the web.config. When SSL is disabled, the form will issue a POST request. When SSL is enabled, jQuery will default to a GET request with JSONP. We need both protocols to be available to our Haunt method.

We’ve also defined the RequireHttpsByConfig to ensure an SSL protected request. As an aside, we’re also using the JsonpFilter to return Json formatted data, wrapped in the JSONP format. Depending on the sensitivity of your data (ie., passwords), you may not want to return it to the client in the Json response, as the above method does (we return the data to display on the client for this example only).

Mutating our Form Action

Since we allow configuring our web application to enable to disable SSL HTTPS via the web.config, we’ll need to adjust our form action parameters to indicate HTTP or HTTPS for the receiving URL. We can do this with a basic C# ASP .NET MVC helper method.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static class Html { public static string GetHttpOrHttps ( this HtmlHelper helper ) { string protocol = "http://" ; string enableSSL = ConfigurationManager.AppSettings[ "EnableSSL" ]; if (! string .IsNullOrEmpty(enableSSL) && enableSSL.ToLower() == "true" ) { protocol = "https://" ; } return protocol; } }

The helper method reads the web.config to determine if SSL HTTPS is enabled. If it is, the protocol is set to HTTPS://, otherwise it defaults to HTTP://. We can then implement our AJAX modal dialog MVC view with the following Razor code:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 < div id= "hauntDialog" title= "Preparing to Haunt (HTTPS)" style= "display: none;" > @using (Html.BeginForm( "haunt" , "ghost" , FormMethod.Post, new { id = "hauntForm" , action = Html.GetHttpOrHttps() + Request.Url.Authority + "/ghost/haunt?callback=?" })) { < div id= "status" style= "color: #ff0000;" > </ div > < div > < div style= "float: left; width: 190px;" > @Html.LabelFor(e => e.HumanName, "Human Target Name:" ) </ div > < div style= "float: left;" > @Html.EditorFor(e => e.HumanName) </ div > </ div > < div style= "clear: both;" ></ div > < div > < div style= "float: left; width: 190px;" > @Html.LabelFor(e => e.DaysToHaunt, "Length of Haunting in Days:" ) </ div > < div style= "float: left;" > @Html.EditorFor(e => e.DaysToHaunt) </ div > </ div > < div style= "clear: both;" ></ div > } </ div >

Notice how the action parameter for the form is overridden to determine HTTP or HTTPS for the receiving MVC controller method.

Giving the App Some Juice

You can run the example C# ASP .NET MVC web application with the EnableSSL property in the web.config set to false. However, this wouldn’t demonstrate the best part about the example. Therefore, you’ll need to enable SSL in IIS for the web application and allow HTTPS SSL in the demo. You can use a self-signed certificate for the demo web application, which is actually very easy to setup in IIS 7.0. You can find a step-by-step example of the setup in Scott Gu’s post Enabling SSL on IIS 7 Using Self Signed Certificates.

Open IIS 7.0 Create a new web site for the demo. Select the root IIS node and double-click on Server Certificates under the heading “IIS” along the middle-column. Along the right-side under Actions, click Create Self-Signed Certificate and provide a name for the new cert. Select your web site node and click Bindings. Add an HTTP (port 80) and HTTPS (port 443) binding. When asked to select the SSL certificate, choose from the drop-down menu the cert you created in step 4 above. Start the web site up (be sure you’re the only one using port 443, otherwise the site may fail to start).

Finally, edit the web.config to set the EnableSSL=true parameter and access the site. Don’t get scared when you click the Enter link and see a web browser warning about the self-signed certificate. The certificate is just a test one that we defined for our development environment and is perfectly safe to use.

The Dark Side

In conclusion, our example demonstrates successfully transitioning from HTTP to HTTPS SSL and back again, safely posting form data from client to server. While the technique of using HTTP pages, by default, and enabling SSL HTTPS only on sensitive pages is somewhat common amongst web applications, it is certainly worth considering using permanent SSL HTTPS across the entire site. There are several primary reasons for doing so. The first is that although the login credentials are posted in secure HTTPS to the server for authentication, by transitioning back to HTTP after logging in, the authentication ticket is left in plain view for each request. The ticket consists of an encrypted combination of the username and password, which although generally secure, could still provide an opportunity to be hijacked. It’s also worth noting that with increasing web server and CPU processing power, the additional processing requirements for SSL requests may actually be of very minor impact to the server. For these reasons, it’s important to consider when to utilize dual HTTP and HTTPS protocols vs continuous SSL HTTPS for the entire duration that the user is logged in.

Download @ GitHub

You can download the project source code on GitHub by visiting the project home page.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.