Hawk Authentication for ASP.NET Web API using Thinktecture.IdentityModel.45

Hawk is a MAC-based HTTP authentication scheme that provides partial cryptographic verification of HTTP messages. Hawk requires a symmetric key to be shared between the client and the server out-of-band. For more info, see here.

Hawk 101

The client sends an HTTP request, like so.

GET /resource/1?b=1&a=2 HTTP/1.1
Host: example.com:8000

The server returns a challenge, like so.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Hawk


The client creates the HTTP authentication header by normalizing the request, like this.

hawk.1.header\n
1353832234\n
j4h3g2\n
GET\n
/resource/1?b=1&a=2\n
example.com\n
8000\n
\n
some-app-ext-data\n

1353832234 is the UNIX time indicating the time the client has created the header. j4h3g2 is a nonce generated by the client. Unlike digest authentication, the client uses a self-generated nonce. Line 4 is the HTTP method followed by the URI path and query string in line 5. Then comes the host and port in the lines 6 and 7. Next to the port will be the hash of the request body. Since this is a GET, there is no body and hence the line 8 is just blank. The last line is the application specific data placeholder that can be used to send any application specific data, as the name indicates. Every line is terminated by a new line ‘\n’ character.

The client calculates the HMAC of the above string (normalized request) and base64 encode it. Say the HMAC is 6R4rV5iE+NPoymIxmo1vpMofpLAE=. Then, it sends the request, like so. I’m wrapping the Authorization header into multiple lines for better readability.

GET /resource/1?b=1&a=2 HTTP/1.1
Host: example.com:8000
Authorization: Hawk id="dh37fgj492je",
                       ts="1353832234",
                          nonce="j4h3g2",
                            ext="some-app-ext-data",
                               mac="6R4rV5iE+NPoymIxmo1vpMofpLAE="

Here, id is the user identifier that the server will use to identity the user. ts, nonce, and ext are as-is from the above normalized request. mac is the HMAC calculated in the previous step.

The client and the server shares the same key. Also, they decide on the algorithm to be used – SHA1 or SHA256. It is important to note that the line 8 in the normalized request above is just a plain hash and not HMAC. Only the normalized request containing this hash is MACed using the algorithm agreed upon and the shared symmetric key.

I took the case of GET in the example above but for a method with request body, say POST, the hash that will fit into line 8 (called payload hash) is calculated using the content type and the request body, like so.

hawk.1.payload\n
application/json\n
{"greeting":"Hello world!"}\n

This normalized payload is hashed (and not MACed) and that is the payload hash which will go right into line 8, before MAC is calculated. Let’s say hash calculated is Yi9LfIIFRtBEP=. Now, the normalized request will be.

hawk.1.header\n
1353832234\n
j4h3g2\n
GET\n
/resource/1?b=1&a=2\n
example.com\n
8000\n
Yi9LfIIFRtBEP=\n
some-app-ext-data\n

The HTTP Authorization header in this case will be as follows and it includes a hash field as well.

Authorization: Hawk id="dh37fgj492je",
                      ts="1353832234",
                        nonce="j4h3g2",
                          hash="Yi9LfIIFRtBEP=",
                            ext="some-app-ext-data",
                              mac="aSe1DEReKjVw="

Now, ASP.NET Web API receives this header. It right away knows the user identifier based on the id field in the header. ASP.NET Web API retrieves the shared key corresponding to this identifier. Then, it basically repeats the previous steps of creating the normalized request and calculating the HMAC. If computed HMAC matches the one in the mac field, authentication is successful.

Hawk also supports the client validating the server response and short-term access to a resource through bewit. There is also a nice mechanism for the server to indicate to the client the server time so that the client can apply an adjustment to reduce the skew between the clocks in the subsequent requests. I hope to cover all those in future blog posts.

Support for Hawk in Thinktecture.IdentityModel.45

Dominick is kind enough to allow me to contribute to Thinktecture.IdentityModel.45. Thinktecture.IdentityModel.45 now supports Hawk authentication. All you need to do is to get the NuGet package. Then, add HawkAuthenticationHandler to the DelegatingHandler collection of your configuration, in the server side. In the client side, use the handler HawkValidationHandler with HttpClientFactory and you are all set.

HawkValidationHandler in the client side creates the Authorization header with necessary artifacts. HawkAuthenticationHandler in the Web API side authenticates the request based on these artifacts. Then, HawkAuthenticationHandler creates the Server-Authorization header with artifacts, which is then validated by HawkValidationHandler in the client side. This symmetricity lends itself well for a handlers-based implementation in both the server and the client side.

Code for self-hosted ASP.NET Web API will be like this. You will need to provide the callback for ttidm to retrieve the credentials (key, algorithm, etc) based on the user ID that gets sent to your method.

static void Main(string[] args)
{
    var configuration = new HttpSelfHostConfiguration(
                                     "http://localhost:12345");

    configuration.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    var credentialStorage = new List<Credential>()
    {
        new Credential()
        {
            Id = "dh37fgj492je",
            Algorithm = SupportedAlgorithms.SHA256,
            User = "Steve",
            Key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"
        }
    };

    var options = new Options()
    {
        ClockSkewSeconds = 60,
        LocalTimeOffsetMillis = 0,
        CredentialsCallback = (id) => credentialStorage
                                        .FirstOrDefault(
                                           c => c.Id == id),
        ResponsePayloadHashabilityCallback = (r) => true,
        VerificationCallback = (request, ext) =>
        {
            if (String.IsNullOrEmpty(ext))
                return true;

            string name = "X-Request-Header-To-Protect";
            return ext.Equals(name + ":" + 
                            request.Headers[name].First());
        }
    };

    configuration.MessageHandlers.Add(
                            new HawkAuthenticationHandler(options));

    using (HttpSelfHostServer server = 
                                new HttpSelfHostServer(configuration))
    {
        server.OpenAsync().Wait();
        Console.WriteLine("Press Enter to terminate the server...");
        Console.ReadLine();
    }
}

ClockSkewSeconds indicate the time window for failing a request for the want of fresh timestamp (to prevent replay attacks).

In the client side, code will be like this. I assume you use HttpClient so that the built-in HawkValidationHandler can be used. If not, you can implement the same functionality that the handler implements in your code. It is just all about calling the HawkClient class.

static void Main(string[] args)
{
    string uri = "http://localhost:12345/values";

    var credential = new Credential()
    {
        Id = "dh37fgj492je",
        Algorithm = SupportedAlgorithms.SHA256,
        User = "Steve",
        Key = "werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn"
    };

    var options = new ClientOptions()
    {
        CredentialsCallback = () => credential,
        RequestPayloadHashabilityCallback = (r) => true,
        NormalizationCallback = (req) =>
        {
            string name = "X-Request-Header-To-Protect";
            return req.Headers.ContainsKey(name) ? 
                        name + ":" + req.Headers[name].First()
                                                      : null;
        }
    };

    var handler = new HawkValidationHandler(options);

    HttpClient client = HttpClientFactory.Create(handler);
    client.DefaultRequestHeaders.Add(
                           "X-Request-Header-To-Protect", "secret");

    var response = client.GetAsync(uri).Result;
    Console.WriteLine(response.Content.ReadAsStringAsync().Result);

    response = client.PostAsJsonAsync(uri, credential.User).Result;
    Console.WriteLine(response.Content.ReadAsStringAsync().Result);

    Console.Read();
}

I have just covered the very basic scenario in this post. As I mentioned before, future posts will cover more.

2 thoughts on “Hawk Authentication for ASP.NET Web API using Thinktecture.IdentityModel.45

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.