January 17, 2014

Shared Key Authentication [Good option to consider implementing auth in REST service]

I have a requirement to implement authentication in a REST service I was developing, I did some investigation on different options and came across the authentication mechanism supported by Windows Azure Storage service and Amazon Web service. These service are using 'Shared Key Authentication', I implemented the same in my project.

The authentication flow is:

1    For each registered users of the REST service, we generates and assign a unique private key. We call it “Shared Secret Key”. This will be known only for user and service. User should never share this key with anyone.  In case of Windows azure blob service this is the ‘primary/secondary access key’.

Note: In this post some times I use the word 'client' for 'user'.

2.      Client combines a set of unique data (elements) defined by the service.
The service also defines how to combine these data for example use new line char as a delimiter between unique data, position of each data, how to represent unavailability of a data etc..
We call the string obtained after combing the unique data as ‘canonicalized string’. As per Wiki, in computer science canonicalization (also called standardization or normalization) is a process for converting data that has more than one possible representation into a "standard", "normal", "canonical form"
Normally the data (elements) which is used to build the canonicalized string:
a.      Will be taken from the HTTP request that client is going to make against the service. These data can include selected subset of standard request headers, custom request headers, relative path in the request URI, query parameters etc...
b.      Will also include client identifier e.g. user id associated with the client. In case of Windows Azure storage service this is ‘storage account name’
We call the process of generating the canonicalized string as CanonicalizationOfHttpRequest.
For our service the format of canonicalized string must be:
                VERB + "\n" +
                Content-Length + "\n"
                Content-MD5 + "\n" +
                Content-Type + "\n" +   
                Date + "\n" +
                CanonicalizedHeaders + "\n"

VERB: The VERB element is the HTTP verb, must be uppercase like GET, POST, and PUT.
Content-Length, Content-MD5 and Content-Type: The values of these standard HTTP headers must be included in the canonicalized string in the order shown in the above format, without the header names. If these http headers are not being specified as part of the request then only the new line character is required.
Date The Date element in the canonicalized string should match with the value of a required header ‘myservice-cm-date’ (you can give some meaningful name to this header). This value is used to handle the re-play attack.
The value of the header ‘myservice-cm-date’ must be the current UTC time in the format yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'.
Another required header is ‘myservice-cm-version’ (again you can give some meaningful name to this header)) we use this to decide which version of service the client want to consume.
The CanonicalizedHeaders element must be generated using ‘myservice-cm-date’ and ‘myservice-cm-version’.  The CanonicalizedHeaders element is constructed by concatenating these headers into a single string. Each element in this string will have the format "header-name:header-value", multiple elements are separated by new line character. The elements should appear in the sorted order (ascending) of header-name. Also the header-name must be lower-case.
CanonicalizedResource: The CanonicalizedResource element format is:
/{user-id}/{resource'-relative-URI-path-without-any-query-parameters}/{canonicalized-query- parameters}
The canonicalized-query-parameters string is generated by:
1. Converting all query parameter names to lowercase.
2. Sort the query parameters by parameter name in ascending order.
3. Include the colon (:) between the parameter name and the value.
4. Append a new line character (\n) after each name-value pair.
3.      Client hash the canonicalized string (generated in step 2) with the private key (step1) assigned to him by the service. Again the algorithm to generate the hash will be defined by the service. For example HMACSHA256, HMACSHA512. This step is called signing of the request. The resulting hash is called signature.

string signature = String.Empty;
string  sharedSecretKey = The-Shared-Secret-Key;
string date = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fff'Z'");

request.Headers.Add("myservice-cm-date", date);
request.Headers.Add("myservice-cm-version", "2013-06-26");

var bytes = Encoding.UTF8.GetBytes(sharedSecretKey);
string base64EncodedSharedSecretKey = Convert.ToBase64String(bytes);
byte[]sharedSecretKeyAsByteArray = Convert.FromBase64String(base64EncodedSharedSecretKey);

string canonicalizedString = HttpRequestCanonicalizer.CanonicalizeHttpRequest(
byte[] dataToMAC = Encoding.UTF8.GetBytes(canonicalizedString);
using (HMACSHA256 hmacsha1 = new HMACSHA256(sharedSecretKeyAsByteArray))
    signature = System.Convert.ToBase64String(hmacsha1.ComputeHash(dataToMAC));

4.      Next step is sending this signature as a part of HTTP request for accessing protected resource in the service. The service defines format and any additional data to be included along with signed canonicalized string. Let’s call this authorization related data as "AuthorizationInformation". Service also defines how to send this authorization related data (In our service we decided to use HTTP Authorization header to send this data)

For our service we defined the format of AuthorizationInformation as below [similar to Azure storage service]:

"SharedKey <public-key>:<signed-canonicalized-string>"

Note that <public-key> is some user-identifiable information which can used to identify who you are. This is the public key. We used the user-id associated with the client in Users table in DB.

request.Headers.Add("Authorization", String.Format("{0} {1}:{2}", "SharedKey", userId, signedCanonicalizedString)));

5.      Once service receives the request it parse the 'Authorization' header to extract the public-key and signed canonicalized string.
Using the public-key service look the user in the DB and retrieve their "shared secret key"

6.      Server generates the canonicalized string by combining the same data together in the same way that the client did.
7.      Server generates the signature by signing canonicalized string using user's shared secret key using the same algorithm.
8.      Service compares the signed string got from the client with the computed string, if they match, then the client is considered legit and allow access to the service. Otherwise reject the request with authentication error

Below diagram shows the flow (this diagram is taken from Amazon web service documentation, but updated to match with the terms we used above)

1 comment:

  1. Love the writeup. Good information. Wondering how common is this mechanism used in other REST apps and and pros and cons?