Note: Code for this example is on Google Docs.
NOTE: DO NOT USE THE PUBLIC/PRIVATE KEYS IN THIS EXAMPLE IN YOUR PRODUCTION APPLICATION!!!! BE SURE TO GENERATE YOUR OWN KEYS!! OTHERWISE, YOUR APPLICATION WILL NOT BE SECURE AS THE PRIVATE KEY WILL BE IN THE FIELD!!!!
Recently, I've been exploring the new ASP.NET Web API. So far, I've been impressed with how easy it is to build RESTful web interfaces. In the examples I've published, none have been secure. In the real world – the world that exists beyond the world of samples and demos – security is a matter than cannot be brushed aside. In this post, I squarely tackle that issue by showing you an approach that locks down and secures your ASP.NET Web API.
In research this topic, looking what others have done this far, I came away with a lot of approaches that in my opinion, where too complicated and quite frankly, a pain to setup. It was also questionable what the value proposition was for all that pain. In other words, how was my site made more secure by implementing one approach or another. Two big topics that I won't discuss today are oAuth and OpenID. Sure, those approaches each solve a segment of the overall problem. oAuth is about authorization. For example, I can use my credentials in Twitter to gain access to another site. The relationship with oAuth is one app authorizing another app. OpenID on the other hand is about authentication. I have an Openid and I use that as the basis for an application to authenticate who I am – without the need for that application to maintain my security details. With OpenID, it's one security mechanism used for many applications. In any application, you need to be both authenticated and authorized. Once a user gets access to an application, the process still needs to be secure – especially if HIPPA related type data is presented.
For many applications, OpenID or oAuth can work fine. But for a Web API, it really does not work. First, RESTful apis are just that – RESTful – which means they are stateless. To be effective, we cannot make assumptions from one call to the next. We have to supply credentials with each and every request, whether it is a GET, POST, PUT or DELETE. Like Any API, a Web API should, in my opinion be self sufficient as to controlling how people get to the site. Indeed, things like oAuth and OpenID can work. However, I think the process can be made simpler – with techniques that have been around a while and are as relevant as ever – with all due respect to their newer counterparts.
HTTPS
Of all the things that can be done to make your API, more secure, requiring it to run over HTTPS is the easiest thing to implement. This does nothing for authorization and authentication – which I will get to in a moment. HTTPS however, does afford encryption that is not present in regular HTTP traffic. With HTTPS, your traffic is far less likely to be subject to packet sniffing eavesdropping attacks. The key is, how can you ensure that your Web API is only accessed via HTTPS? The answer lies in one of the most useful features in ASP.NET MVC – that has also been implemented in ASP.NET Web API: Action Filters. And in this case, I'll make the action filter a global action filter – eliminating the need to manually add the filter to every controller action.
Here is the custom action filter
using System; using System.Linq; using System.Net.Http; using System.Web.Http.Filters; using System.Web.Http.Controllers; namespace WebAPI { public class CustomHttpsAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (!String.Equals(actionContext.Request.RequestUri.Scheme, "https" , StringComparison.OrdinalIgnoreCase)) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent( "HTTPS Required" ) }; return ; } } } } |
Nothing very complicated going on here. If https is not present in the URL, a response is fabricated indicating a bad request (error 400). A message is also added to the response content as well.
As the previous figure illustrates, when we attempt to navigate to the Web API with HTTP, as opposed to HTTPS, a response with status code 400 results – along with a friendly message in the response body that describes the error.
Tokens based on Public/Private Keys
Of all the ways to authorize and authenticate, it seems to me that tokens have done a good at this task. A token can be used to authorize, and for the most part, authenticate a user. Of course, it is possible for a key to get passed around. I'll address that in the next section. That said, it may be perfectly fine to assume that if a visitor has a properly encrypted token, that visitor is who he/she/it claims to be (in cases where its an application, not a person, it is the right pronoun!!)
The idea behind a private and public key pair is simple. A token is encrypted with the public key. In some cases, the public key sits in the field. In cases where data has to be encrypted by the client to be decrypted on the server, the public key needs to be in the field. The server is the only thing that should have the private key. It's the private key that handles the decryption. To get this working, we need a simple class to handle our encryption needs:
using System; using System.Linq; using System.Security.Cryptography; using System.Text; namespace WebAPI { public class RSAClass { private static string _privateKey = "<RSAKeyValue><Modulus>s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=</Modulus><Exponent>AQAB</Exponent><P>3J2+VWMVWcuLjjnLULe5TmSN7ts0n/TPJqe+bg9avuewu1rDsz+OBfP66/+rpYMs5+JolDceZSiOT+ACW2Neuw==</P><Q>0HogL5BnWjj9BlfpILQt8ajJnBHYrCiPaJ4npghdD5n/JYV8BNOiOP1T7u1xmvtr2U4mMObE17rZjNOTa1rQpQ==</Q><DP>jbXh2dVQlKJznUMwf0PUiy96IDC8R/cnzQu4/ddtEe2fj2lJBe3QG7DRwCA1sJZnFPhQ9svFAXOgnlwlB3D4Gw==</DP><DQ>evrP6b8BeNONTySkvUoMoDW1WH+elVAH6OsC8IqWexGY1YV8t0wwsfWegZ9IGOifojzbgpVfIPN0SgK1P+r+kQ==</DQ><InverseQ>LeEoFGI+IOY/J+9SjCPKAKduP280epOTeSKxs115gW1b9CP4glavkUcfQTzkTPe2t21kl1OrnvXEe5Wrzkk8rA==</InverseQ><D>HD0rn0sGtlROPnkcgQsbwmYs+vRki/ZV1DhPboQJ96cuMh5qeLqjAZDUev7V2MWMq6PXceW73OTvfDRcymhLoNvobE4Ekiwc87+TwzS3811mOmt5DJya9SliqU/ro+iEicjO4v3nC+HujdpDh9CVXfUAWebKnd7Vo5p6LwC9nIk=</D></RSAKeyValue>" ; private static string _publicKey = "<RSAKeyValue><Modulus>s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>" ; private static UnicodeEncoding _encoder = new UnicodeEncoding(); public static string Decrypt( string data) { var rsa = new RSACryptoServiceProvider(); var dataArray = data.Split( new char [] { ',' }); byte [] dataByte = new byte [dataArray.Length]; for ( int i = 0; i < dataArray.Length; i++) { dataByte[i] = Convert.ToByte(dataArray[i]); } rsa.FromXmlString(_privateKey); var decryptedByte = rsa.Decrypt(dataByte, false ); return _encoder.GetString(decryptedByte); } public static string Encrypt( string data) { var rsa = new RSACryptoServiceProvider(); rsa.FromXmlString(_publicKey); var dataToEncrypt = _encoder.GetBytes(data); var encryptedByteArray = rsa.Encrypt(dataToEncrypt, false ).ToArray(); var length = encryptedByteArray.Count(); var item = 0; var sb = new StringBuilder(); foreach (var x in encryptedByteArray) { item++; sb.Append(x); if (item < length) sb.Append( "," ); } return sb.ToString(); } } } |
In this simple RSAClass, I have embedded the public/private RSA Parameters. To create and export new parameters, you would issue the following code outlined in the next image:
[Test] public void RSAParameters() { var rsa = new RSACryptoServiceProvider(); var privateParameters = rsa.ExportParameters( true ); var publicParameters = rsa.ExportParameters( false ); //Export private parameter XML representation of privateParameters //object created above Debug.WriteLine(rsa.ToXmlString( true )); //Export private parameter XML representation of publicParameters //object created above Debug.WriteLine(rsa.ToXmlString( false )); } |
The following is a simple test that illustrates how to encrypt and decrypt a string with the public/private key pair:
[Test] public void VerifyToken() { var token = "User1" ; var encryptedToken = RSAClass.Encrypt(token); var decryptedToken = RSAClass.Decrypt(encryptedToken); Assert.AreEqual(token,decryptedToken); } |
In this case, the encrypted 128 byte token that results for "User1″ is:
57,46,60,70,93,230,85,33,98,19,10,46,84,91,218,43,207,42,159, 167,5,25,157,4,224,142,235,8,160,199,123,100,107,58,37,204,133, 81,138,196,237,190,56,119,158,7,224,89,84,85,208,169,44,179,102, 218,55,60,76,134,144,22,208,230,165,179,83,125,86,57,224,42, 29,58,188,45,73,33,160,87,165,105,131,139,132,137,209,67,92,36, 168,73,176,205,251,48,240,228,14,39,197,36,42,21,216,242,172, 4,160,234,138,77,156,28,191,63,111,207,221,31,103,213,58,62, 186,123,221,230 |
You may wish to modify this by having a smaller encrypted token. This is but one approach of many that are possible.
The next thing we need is an action filter to make sure the token is present in each and every request:
using System; using System.Linq; using System.Net.Http; using System.Web.Http.Filters; using System.Web.Http.Controllers; using WebAPI.Models; namespace WebAPI { public class TokenValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { string token; try { token = actionContext.Request.Headers.GetValues( "Authorization-Token" ).First(); } catch (Exception) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) { Content = new StringContent( "Missing Authorization-Token" ) }; return ; } try { AuthorizedUserRepository.GetUsers().First(x => x.Name == RSAClass.Decrypt(token)); base .OnActionExecuting(actionContext); } catch (Exception) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { Content = new StringContent( "Unauthorized User" ) }; return ; } } } } |
There are two levels of checking here. The first is whether or not the authorization token is present in the header. If not, a bad request response (error 400) is returned. If the token was provided but does not resolve to an authorized user, then an unauthorized response (error 401) is returned. The following illustrates how to setup Fiddler with the Authorization-Token in the header:
The following illustrates the results of the call in the previous image:
For purposes of this demo, I established a simple authorized user repository that has a list of pre-defined users:
using System; using System.Collections.Generic; using System.Linq; namespace WebAPI.Models { public class AuthorizedUserRepository { public static IQueryable<User> GetUsers() { IList<User> users = new List<User>(); users.Add( new User( "User1" )); users.Add( new User( "User2" )); users.Add( new User( "User3" )); users.Add( new User( "Administrator" )); return users.AsQueryable(); } } } |
Within this approach, there are all sorts of variants you could employ. For example, you could swap out the public/private keys on some periodic basis. Of course, that means you will have to issue new tokens to users who have access to your API!!
IP Filtering
Up to this point, while the system is pretty secure, it can be locked down further. Right now, tokens could be moved from location to location. It may be that you only want to authorize traffic from certain IP addresses and hosts. For purposes of this demo, I created a simple Authorized IP Repository:
using System; using System.Collections.Generic; using System.Linq; namespace WebAPI.Models { public class AuthorizedIPRepository { public static IQueryable< string > GetAuthorizedIPs() { var ips = new List< string >(); ips.Add( "127.0.0.1" ); ips.Add( "::1" ); return ips.AsQueryable(); } } } |
In actual practice, I might take the extra step of linking specific tokens to a specific IP range. I'll leave that exercise to you! The following is the action filter that restricts traffic from a defined IP list:
using System; using System.Linq; using System.Web.Http.Controllers; using System.Web.Http.Filters; using System.Net.Http; using WebAPI.Models; namespace WebAPI { public class IPHostValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { var context = actionContext.Request.Properties[ "MS_HttpContext" ] as System.Web.HttpContextBase; string userIP = context.Request.UserHostAddress; try { AuthorizedIPRepository.GetAuthorizedIPs().First(x => x == userIP); } catch (Exception) { actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) { Content = new StringContent( "Unauthorized IP Address" ) }; return ; } } } } |
To make these action filters global, the following code in the Global.asax Application_Start() will do the trick:
var config = GlobalConfiguration.Configuration; config.Filters.Add( new TokenValidationAttribute()); config.Filters.Add( new CustomHttpsAttribute()); config.Filters.Add( new IPHostValidationAttribute()); |
If you wished to initiate a request from your C# code, you might have a utility method like this:
WebRequest getRequest( string method, string contentType, string endPoint) { var request = WebRequest.Create(endPoint); request.Method = method; request.ContentType = contentType; ServicePointManager.ServerCertificateValidationCallback = delegate { return true ; }; request.Headers.Add( "Authorization-Token" , "57,46,60,70,93,230,85,33,98,19,10,46,84,91,218,43,207,42,159,167,5,25,157,4,224,142,235,8,160,199,123,100,107,58,37,204,133,81,138,196,237,190,56,119,158,7,224,89,84,85,208,169,44,179,102,218,55,60,76,134,144,22,208,230,165,179,83,125,86,57,224,42,29,58,188,45,73,33,160,87,165,105,131,139,132,137,209,67,92,36,168,73,176,205,251,48,240,228,14,39,197,36,42,21,216,242,172,4,160,234,138,77,156,28,191,63,111,207,221,31,103,213,58,62,186,123,221,230" ); return request; } |
This request has the necessary information to make an API request
That's pretty much it. Now we have a ASP.NET Web API that requires requests to be under the HTTPS protocol, requires an encrypted authorization token and requires traffic to only come from a predefined population of IP addresses. There are any number of ways you can vary this implementation. My goal with this post was to make it easy to get started. With this approach, you don't have to worry about cumbersome query string parameters, etc. In this example, I am putting authentication details where they belong – in the header – which means it will work for anything. No need to deal with clumsy query string parameters either.