Security is not easy. There are a lot of options when it comes to implementing web security and it takes a rather deep understanding of the security landscape just to be able to even pick the right technology for your specific case. In the most recent feat to secure my new Web API project, I ended up spending more than a week reading about web security and trying to find the right solution for my problem and getting grasp of security fundamentals.
My goal was to use Mozilla Person as an Identity Provider. However once user was authenticated with Persona I needed to be able to issue Web API requests.
The interesting thing about Web API is that it doesn’t support sessions, and while it is possible to enable session state, it is not a recommended practice, since the idea of the Web API is to embrace the statelessness of the HTTP protocol. This means that one needs to authenticate each HTTP request separately and independently of other requests.
Our options boil down to these three
Basic authentication. The problem with Basic authentication, however is that it sends credentials in plaintext (or the value might be hashed) and therefore is vulnerable to Man In The Middle attacks and replay attacks. So unless you are using TLS, Basic authentication isn’t a viable option.
Digest access authentication. This is a viable solution and suitable for authentication over non-secure channels (without TLS). The downside of this approach is that it generates quite a bit more HTTP requests to perform the handshake and therefore can be noticeably slower on lower Internet connection speeds.
HMAC authentication. This is the recommended strategy for clients not using TLS. With this approach, each HTTP is augmented with a limited-lifetime authentication token. This token is generated cryptographically and therefore is not susceptible to MITM attacks. The replay attacks are possible, but its risk can be minimized by limiting the token’s expiration to a short window of time (e.g. - 60 seconds).
After extensive reading, it was clear to me that HMAC authentication was the way to go. Luckily there is a very nice implementation of the HMAC authentication - Hawk - which was written by Eran Hammer (one of the original creators of OAuth). For our use case, all that is needed, is the browser.js file, which will take care of signing the requests.
On the server side we would need a mechanism to decrypt the Hawk authentication token and to validate it. This is easily done with Thinktecture.IdentityModel.Hawk nuget.
Below is the detailed description of the steps I’ve taken to implement basic setup of the Hawk authentication for my Web API.
1. Pull in your dependencies
Once you you have created your Web API project, you will need to
Install-Package Thinktecture.IdentityModel.Hawk
Then you will need to copy browser.js into your ~/scripts folder.
2. Create ApiController
Now let’s create the HelloWorldController which we will want to protect:
public class HelloController : ApiController
{
[Authorize]
public string Get()
{
return "Hi " + this.User.Identity.Name + "!";
}
}
3. Set project properties
Next you’ll want to ensure that your project properties resemble the ones in the screenshot:
4. Enable Hawk authentication
To make things simpler I created this class
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using Thinktecture.IdentityModel.Hawk.Core;
using Thinktecture.IdentityModel.Hawk.Core.Helpers;
using Thinktecture.IdentityModel.Hawk.Core.MessageContracts;
using Thinktecture.IdentityModel.Hawk.WebApi;
public class HawkAuthenticator
{
public static void EnableHawkAuthentication(HttpConfiguration config)
{
var options = new Options
{
ClockSkewSeconds = 60,
LocalTimeOffsetMillis = 0,
CredentialsCallback = CredentialsCallback,
ResponsePayloadHashabilityCallback = r => true,
VerificationCallback = OnVerificationCallback
};
var handler = new HawkAuthenticationHandler(options);
config.MessageHandlers.Add(handler);
}
private static bool OnVerificationCallback(IRequestMessage request, string ext)
{
if (string.IsNullOrEmpty(ext))
{
return true;
}
const string Name = "X-Request-Header-To-Protect";
return ext.Equals(Name + ":" + request.Headers[Name].First());
}
private static Credential CredentialsCallback(string id)
{
var credentialStorage = new List
{
new Credential
{
Id = "123",
Algorithm = SupportedAlgorithms.SHA256,
User = "John",
Key = "secret"
}
};
return credentialStorage.FirstOrDefault(c => c.Id == id);
}
}
and the called it from the ~/App_Start/WebApiConfig.cs:
public class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional });
HawkAuthenticator.EnableHawkAuthentication(config);
}
}
Now our server is ready to accept requests authenticated with Hawk token.
5. Write the client
Now when making requests to the Web API, we need to sign each request using browser.js like so:
<a id="gethello" href="#">say hello</a>
<script src="/Scripts/jquery.js"></script>
<script src="/Scripts/browser.js"></script>
<script>
(function () {
var url = 'http://localhost:50374/api/hello';
var credentials = {
id: '123',
key: 'secret',
algorithm: 'sha256'
};
var req = {
method: 'GET',
url: url,
host: 'localhost',
port: 50374
};
$("#gethello").click(function () {
var header = hawk.client.header(url, req.method, { credentials: credentials });
$.ajax({
dataType: "json",
headers: { Authorization: header.field },
url: url,
success: function (error, response, body) {
var isValid = hawk.client.authenticate(body, credentials, header.artifacts, {
payload: body.responseText,
required: true
});
console.log(isValid ? body.responseText : "not valid");
}
});
});
}());
</script>
And that’s it.
If you inspect the http request you will see each request signed with the Authorization
header and server responds with the Server-Authorization
header, which can be used by the client to ensure that the response is authentic.
I am by no means a security professional, so if you spot any problems with any of my advice, I will be more than happy to hear it in the comments below. Cheers!