The updated code for the Web API project can be found here In the last post on Making Your ASP.NET Web API’s Secure, I received some very useful comments. Thanks to Pedro Reys for pointing out my non-use of HttpMessageHandlers. Pedro is absolutely correct in his observation that through the use of handlers, we can detect issues BEFORE processing gets into the controller context. If you are concerned about performance (and we all should be!!) – Http Message Handling is where you want to be. It turned out to be a pretty easier conversion. As you may recall, there were 3 actions filters that enforced:
- HTTPS
- A valid authorization token
- A valid IP Host source
Here are the 3 new handlers:
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace WebAPI
{
public class HttpsHandler : DelegatingHandler
{
protected override Task SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (!String.Equals(request.RequestUri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
return Task.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("HTTPS Required")
};
});
}
return base.SendAsync(request, cancellationToken);
}
}
}
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using WebAPI.Models;
namespace WebAPI
{
public class TokenValidationHandler : DelegatingHandler
{
protected override Task SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
string token;
try
{
token = request.Headers.GetValues("Authorization-Token").FirstOrDefault();
}
catch (System.InvalidOperationException)
{
return Task.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.BadRequest)
{
Content = new StringContent("Missing Authorization-Token")
};
});
}
try
{
var foundUser = AuthorizedUserRepository.GetUsers().FirstOrDefault(x => x.Name == RSAClass.Decrypt(token));
if (foundUser == null)
return Task.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.Forbidden)
{
Content = new StringContent("Unauthorized User")
};
});
}
catch (RSAClass.RSAException)
{
return Task.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent("Error encountered while attempting to process authorization token")
};
});
}
return base.SendAsync(request, cancellationToken);
}
}
}
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using WebAPI.Models;
namespace WebAPI
{
public class IPHostValidationHandler : DelegatingHandler
{
protected override Task SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken) {
var context = request.Properties["MS_HttpContext"] as System.Web.HttpContextBase;
string userIP = context.Request.UserHostAddress;
var foundIP = AuthorizedIPRepository.GetAuthorizedIPs().FirstOrDefault(x => x == userIP);
if (foundIP == null)
return Task.Factory.StartNew(() =>
{
return new HttpResponseMessage(HttpStatusCode.Forbidden)
{
Content = new StringContent("Unauthorized IP Address")
};
});
return base.SendAsync(request, cancellationToken);
}
}
}
I went ahead and created a specific exception to address situations when the RSA encryption/description process throws the generic bad data exception. Here is the revised RSAClass:
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace WebAPI
{
public class RSAClass
{
private static string _privateKey = "s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=AQAB</pre>
3J2+VWMVWcuLjjnLULe5TmSN7ts0n/TPJqe+bg9avuewu1rDsz+OBfP66/+rpYMs5+JolDceZSiOT+ACW2Neuw==
<pre><q>0HogL5BnWjj9BlfpILQt8ajJnBHYrCiPaJ4npghdD5n/JYV8BNOiOP1T7u1xmvtr2U4mMObE17rZjNOTa1rQpQ==</q>jbXh2dVQlKJznUMwf0PUiy96IDC8R/cnzQu4/ddtEe2fj2lJBe3QG7DRwCA1sJZnFPhQ9svFAXOgnlwlB3D4Gw==evrP6b8BeNONTySkvUoMoDW1WH+elVAH6OsC8IqWexGY1YV8t0wwsfWegZ9IGOifojzbgpVfIPN0SgK1P+r+kQ==LeEoFGI+IOY/J+9SjCPKAKduP280epOTeSKxs115gW1b9CP4glavkUcfQTzkTPe2t21kl1OrnvXEe5Wrzkk8rA==HD0rn0sGtlROPnkcgQsbwmYs+vRki/ZV1DhPboQJ96cuMh5qeLqjAZDUev7V2MWMq6PXceW73OTvfDRcymhLoNvobE4Ekiwc87+TwzS3811mOmt5DJya9SliqU/ro+iEicjO4v3nC+HujdpDh9CVXfUAWebKnd7Vo5p6LwC9nIk=";
private static string _publicKey = "s6lpjspk+3o2GOK5TM7JySARhhxE5gB96e9XLSSRuWY2W9F951MfistKRzVtg0cjJTdSk5mnWAVHLfKOEqp8PszpJx9z4IaRCwQ937KJmn2/2VyjcUsCsor+fdbIHOiJpaxBlsuI9N++4MgF/jb0tOVudiUutDqqDut7rhrB/oc=AQAB";
private static UnicodeEncoding _encoder = new UnicodeEncoding();
public static string Decrypt(string data)
{
try
{
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);
}
catch (Exception)
{
throw new RSAException();
}
}
public static string Encrypt(string data)
{
try
{
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();
}
catch (Exception)
{
throw new RSAException();
}
}
public class RSAException : Exception {
public RSAException() : base("RSA Encryption Error") {}
}
}
}
The one difference I did notice between the action filter and handler was in the way unhandled errors were passed back to the client. For example, with an action filter, a missing authorization token would have the following rendered to the client:

On the other and, the message handler would render the same error the client this way:

You may have noticed I changed the linq calls from First to FirstOrDefault. I got flamed a bit for using just First – and then letting the exception deal with the results in the event a value could not be found. I still contend that using an exception or checking the nullity of a value doesn’t make that much of a difference. In the case of checking for the existence of a header, it’s a moot point because if you invoke code like this:
var token =
request.Headers.GetValues("Authorization-Token").FirstOrDefault();
and the Authorization-Token header does not exist, you never get to the FirstOrDefault() Linq Query…:-) As it turns out, you have to wrap that call in a try catch anyway – at least if you wish to present to the client a predictable and useful message. Always take with a grain of salt when somebody espouses “best practice” or something being a “bad practice” or “bad form”. No question, there are established good/best practices out there. Those practices however, are quantifiable and verifiable through empirical evidence. Anything else is simply a matter of opinion and preference. AND – context means everything. What may be a good/best practice in one scenario may not be so in another scenario. And often, technology has little to nothing to do with the equation. For example, Dependency Injection is a good practice and is often, a best practice. BUT – in a shop where the concept is new, foreign or not understood, if it presents a bar to shipping software, it may not be a best practice for that shop – at that time. One of the many reasons why I quickly brush off notions of what best practices are and are not when asked..
No comments:
Post a Comment
Thank's!