For any application which is hosted on the web, it is essential that security is built in from the start.
Enabling your web application to serve secure traffic over HTTPS and enforcing this policy is one of the first things that you should implement and this is just as important for web apps as it is for APIs.
With well-established services like Let’s Encrypt which provide free TLS certificates, there is no longer any compelling technical or financial reason to not use HTTPS for your web applications.
In this article, I demonstrate how to correctly configure your web applications such that they require HTTPS and I cover examples for both ASP.NET and ASP.NET Core projects.
HTTPS Redirection
In order to make sure that your site is properly secured, after obtaining and installing a shiny new SSL/TLS certificate, you’ll want to make sure that HTTPS is always being used.
For your web apps e.g. MVC applications, you can configure traffic to be redirected from HTTP to HTTPS.
For a variety of reasons, it is still advisable to keep the default HTTP port (port 80) open and redirect your users to the default HTTPS port (port 443) for web applications that are accessed via a browser.
This is part of official best practice advice within the Let’s Encrypt documentation. Top security researcher, Scott Helm, also has a very good article that explains why closing port 80 is bad for security in the context of web applications.
In the sections below I demonstrate how to configure HTTPS redirection for ASP.NET and ASP.NET Core web applications.
ASP.NET web apps
The ASP.NET MVC framework conveniently has a built-in RequireHttpsAttribute
class which we can avail of.
When applied, the Attribute will cause a redirect response to be sent back to the client if a request was sent over HTTP instead of HTTPS.
The Attribute can be applied either on a per-controller or per-action basis. However, I highly recommend applying the Attribute globally to enforce HTTPS redirection across the entire site, as shown below.
filters.Add(new RequireHttpsAttribute());
The above line of code typically appears within a static RegisterGlobalFilters
method in a FilterConfig
class, as per the code snippet below.
/// <summary> /// Registers Global Filters. /// </summary> /// <param name="filters">The collection of filters to register</param> public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new RequireHttpsAttribute()); filters.Add(new AuthorizeAttribute()); }
In an ASP.NET application, the RegisterGlobalFilters
method is usually called on start-up from the Application_Start
method within the Global.asax file.
Before moving on, it is important to note that global filters will only apply to HTTP requests which are passed to your controllers. Therefore it is still possible for a client to access static files such as stylesheets and script files over insecure HTTP.
You should take care to use relative links to any static resources which you are referencing from your HTML, or use absolute URLs with the HTTPS scheme to make sure that all content is being served securely.
Rewrite rules
To make your web application even more secure you can configure redirection at the reverse proxy level e.g. as part of your IIS configuration. This will make sure that all incoming requests get redirected appropriately.
In the case of IIS, you can implement this via a rewrite rule by adding the following to your Web.config file.
<rewrite> <rules> <rule name="HTTP to HTTPS redirect" stopProcessing="true"> <match url="(.*)" /> <conditions> <add input="{HTTPS}" pattern="off" ignoreCase="true" /> </conditions> <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" /> </rule> </rules> </rewrite>
IIS will respect the above rewrite rule when handling incoming traffic, redirecting insecure traffic to HTTPS before it reaches your application code.
As with many aspects of security, I believe that it is best to have multiple layers of security in place. If something fails or is misconfigured at one level, having a fallback is always a good thing.
.NET Core
.NET Core also has a built-in RequireHttpsAttribute
class which can be applied either on a per-controller/action basis or it can be registered globally.
Global registration can be set up within the ConfigureServices
method of your Startup
class, as demonstrated below.
/// <summary> /// This method gets called by the runtime. /// Use this method to add services to the container. /// </summary> /// <param name="services">The collection of container services</param> public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(options => options.Filters.Add(new RequireHttpsAttribute())); }
The above code does the same thing fundamentally as in a traditional ASP.NET project.
However, in .NET Core, we can do better than this.
.NET Core also has built-in HTTPS Redirection middleware, which can be configured with one line of code, as shown below.
app.UseHttpsRedirection();
The above line of code should be added to the Configure
method of the Startup
class. Most of the standard ASP.NET Core web application templates e.g. for MVC, configure the HTTPS Redirection middleware automatically.
Below is an example of the typical contents of the Configure
method for reference.
/// <summary> /// This method gets called by the runtime. /// Use this method to configure the HTTP request pipeline. /// </summary> /// <param name="app">The application builder object used to configure the request pipeline</param> /// <param name="env">The web hosting environment the application is running in</param> public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
So why is the HTTPS Redirection middleware better than the RequireHttpsAttribute
filter?
Well, thanks to the way that ASP.NET Core web apps are hosted, the middleware HTTPS redirects are applied at a higher level and therefore requests for static files such as stylesheets and scripts will also be redirected in addition to controller-bound requests.
HSTS
Another useful piece of ASP.NET Core middleware is the HSTS middleware and it is configured in the example above via the following line of code.
app.UseHsts();
Note that as with many of the built-in middleware components, many more advanced aspects of ASP.NET Core middleware can be configured within the ConfigureServices
method of your Startup class.
What is HSTS and why should we use it?
The problem with HTTPS redirection on its own is with that first insecure request that comes into the application from a client.
If the first incoming HTTP request gets intercepted by a ‘man in the middle’ the integrity of the request will be lost. For example, the client could be redirected somewhere else without them noticing e.g. to a fake login page.
HSTS stands for HTTP Strict Transport Security and it helps solve the problem described above by informing the browser that a web application should only be accessed over HTTPS.
It does this by returning a Strict-Transport-Security header in the response so that subsequent requests use HTTPS without any further redirections. The browser caches this instruction to ensure that further visits to the site will be over HTTPS without any more redirects.
The very first request
Yes, that all sounds great, but what about that very first request?
We’ve solved most of the problem, but we still haven’t dealt with the very first request received from a client who has never visited our site before.
In order to be super-secure and close the loop on this, we can register our site to be ‘preloaded’. Browsers keep a list of sites that are on a ‘preload’ list and if your site is on this list then you’ll never receive an insecure request from a client as the browser will honour the preload status.
You can register your site to be preloaded by going to the HSTS Preload website and submitting a request to have it preloaded.
Below is an example of an HSTS header with the preload directive specified.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Note that it is extremely important that you test your site thoroughly before enabling HSTS in a production environment to ensure that it is working correctly over HTTPS. This is especially important if you choose to preload your site as this cannot be easily undone and it could take months to get your site removed from the preload list.
I highly recommend configuring HSTS for your web applications. Whether or not you also choose to have your site preloaded will depend on your specific security requirements.
Outbound rules
As per the Rewrite rules section from earlier in this article, you can also enable HSTS at the reverse proxy level.
Below is an example of how to configure this within a Web.config file if you are hosting your application in IIS.
<rewrite> <outboundRules> <rule name="Add Strict-Transport-Security when HTTPS" enabled="true"> <match serverVariable="RESPONSE_Strict_Transport_Security" pattern=".*" /> <conditions> <add input="{HTTPS}" pattern="on" ignoreCase="true" /> </conditions> <action type="Rewrite" value="max-age=31536000" /> </rule> </outboundRules> </rewrite>
Now the HSTS header will be set for all HTTPS traffic on your site.
Note that the above approach will work for both traditional ASP.NET and ASP.NET Core applications. You just need to add a Web.config file to your project and make sure that that the ‘Copy to Output Directory’ property is set to ‘Copy if Newer’.
APIs
APIs differ quite a bit from normal web applications which are accessed by humans, both in their design, intended use-cases and security considerations.
We’ll cover the best practices for requiring HTTPS for APIs in the sections below.
Requiring HTTPS?
Traditional ASP.NET Web API projects do not have access to a built-in Attribute for requiring HTTPS.
However, this doesn’t stop us from creating our own.
Below is an example of how to do this.
/// <summary> /// Called when a process requests authorization. /// </summary> /// <param name="actionContext">The action context</param> public override void OnAuthorization(HttpActionContext actionContext) { HttpRequestMessage request = actionContext.Request; if (request.RequestUri.Scheme != Uri.UriSchemeHttps) { if (request.Method.Equals(HttpMethod.Get)) { actionContext.Response = request.CreateResponse(HttpStatusCode.Found, "SSL is required"); // Provide the correct URL to the user via the Location header. var uriBuilder = new UriBuilder(request.RequestUri) { Scheme = Uri.UriSchemeHttps, Port = 443 }; actionContext.Response.Headers.Location = uriBuilder.Uri; } else actionContext.Response = request.CreateResponse(HttpStatusCode.NotFound, "SSL is required"); } }
In the above example, I have created a class called RequireHttpsAttribute
which derives from the AuthorizationFilterAttribute
class and I’m overriding the virtual OnAuthorization
method.
For GET requests, the above method informs clients of the correct URL using the HTTPS scheme by returning this in the Location header. For all other requests, the message ‘SSL is required’ is simply returned.
Although this makes sense when calling an API from a browser, one of the problems with the above code is that it is returning a redirect status code which most API clients will not understand.
Depending on your viewpoint, the above code could instead be amended to set the HTTP Status Code to something else, such as Bad Request, rather than trying to redirect the client.
We can implement something similar in .NET Core by creating our own class such as ApiRequireHttpsAttribute
which derives from the built-in RequireHttpsAttribute
class. We can then override the virtual HandleNonHttpsRequest
method and set the response code accordingly, as per the sample code below.
/// <summary> /// Called if the request is not received over HTTPS. /// </summary> /// <param name="filterContext">The filter context</param> protected override void HandleNonHttpsRequest(AuthorizationFilterContext filterContext) { filterContext.Result = new StatusCodeResult(400); }
The above code sets the HTTP Status Code for the response to Bad Request (400). It could be amended to include a message in the response or whatever other custom logic is required.
Not listening?
Ok, so we’ve looked at how to force API clients to use HTTPS whenever they attempt to use HTTP.
However, is this the best option?
Best practice dictates that we should have multiple layers of security, but it also dictates that we should implement security as early as possible.
Actually, the most secure thing we can do in the context of APIs is to prevent them from listening on a non-secure port in the first place. This advice is mirrored in the Microsoft Docs for ASP.NET Core.
Of course, we can’t prevent an API client from trying to reach our API in an insecure fashion. However, in this case, it is the fault of the API integrator and not something for which we can do anything meaningful to prevent.
Summary
In this article, I have covered how to enforce HTTPS across your web apps and APIs and I have discussed the most appropriate and secure solutions for both scenarios.
The key thing to take away is that you should implement multiple layers of security for your application and where possible enforce security as early as possible.
Make use of HSTS and leverage the power of your reverse proxy configuration to handle requests in a secure manner.
As always, I trust that you have found this content useful and please leave a comment if you have any questions.
Comments