Implementing [RequireHttps] with ASP.NET Web API
Quick post today. MVC developers are used to the [RequireHttps] attribute, which is an authorization filter which doesn’t allow any requests to be made over "plain” HTTP. This attribute doesn’t exist in Web API, but it’s fairly simple to replicate the same behavior using an authorization filter (an action filter would work just as well, but since the MVC attribute is an IAuthorizationFilter, we did the same here). By having it as an attribute we can simply apply it to any controller or action, and all requests to that controller (or action) will pass through this attribute.
The logic is simple – if the request comes via HTTPS, nothing happens. If it doesn’t, then there are two possible alternatives: if the request verb is GET (or HEAD), then the filter bypasses the operation, and responds to the request with a redirect response, and most HTTP stacks will resend the request to the location specified. If the request verb is not GET (or HEAD) , then we could also return a redirect response, but the problem is that many of the HTTP stacks (I don’t know if all of them, but I tried quite a few) issue a GET request in response to a Redirect from non-GET requests (since per the HTTP RFC they cannot send a non-GET request after redirecting without user confirmation), so that’s not what we want. In this case, we simply return an error response instead.
- public class RequireHttpsAttribute : AuthorizationFilterAttribute
- {
- public override void OnAuthorization(HttpActionContext actionContext)
- {
- var request = actionContext.Request;
- if (request.RequestUri.Scheme != Uri.UriSchemeHttps)
- {
- HttpResponseMessage response;
- UriBuilder uri = new UriBuilder(request.RequestUri);
- uri.Scheme = Uri.UriSchemeHttps;
- uri.Port = 443;
- string body = string.Format("<p>The resource can be found at <a href=\"{0}\">{0}</a>.</p>",
- uri.Uri.AbsoluteUri);
- if (request.Method.Equals(HttpMethod.Get) || request.Method.Equals(HttpMethod.Head))
- {
- response = request.CreateResponse(HttpStatusCode.Found);
- response.Headers.Location = uri.Uri;
- if (request.Method.Equals(HttpMethod.Get))
- {
- response.Content = new StringContent(body, Encoding.UTF8, "text/html");
- }
- }
- else
- {
- response = request.CreateResponse(HttpStatusCode.NotFound);
- response.Content = new StringContent(body, Encoding.UTF8, "text/html");
- }
- actionContext.Response = response;
- }
- }
- }
That’s it. The behavior is almost identical to the MVC [RequireHttps] attribute, except that for non-GET requests the MVC attribute returns a 500 (Internal Server Error) response, while this implementation returns a 404 (Not Found) – thanks to @pmhsfelix for the insight on this.
Comments
- Anonymous
May 03, 2012
Thanks for the article. But not all sites use 443 post for HTTPS. Have a look at nopCommerce implementation (www.nopCommerce.com) of RequireHttpsAttribute. - Anonymous
May 09, 2012
You can always pass an optional parameter to the attribute specifying the port to use for the HTTPS redirect. - Anonymous
November 26, 2012
How to display the message in jsonresult type with validatemodel with action filter + web -api - Anonymous
April 28, 2014
Great article! Helped me out immensely. Question: Why set a 404 status (Not Found)? Why not a 403 (Forbidden)? - Anonymous
August 22, 2014
Thank you for the article. I face just this problem. How do I implement this code?Thanks,Matt - Anonymous
September 08, 2014
This does not work properly, since the UriBuilder does not handle the port correctly. If you pass "http://localhost:1234/api/samples" and resulting URI should be "https://localhost:1234/api/samples", but it's in fact "https://localhost/api/samples". Did you try this yourself? - Anonymous
September 08, 2014
My comment above can be deleted. The UriBuilder does handle the port correctly. However, the Uri class does not include the port in the URI string if it's a default one, such as 443 or 80 (the IsDefault property is set to try). I did not see the port in the Location string for instance, but the actual URI was correct, since 443 was the default port for HHTPS. It didn't work for me because I didn't have SSL properly figured for the project.