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"
CanonicalizedResource
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'.
CanonicalizedHeaders:
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(
absoluteRequestUri,
userId,
httpVerb,
contentLength,
contentMD5,
contentType,
date,
request.Headers);
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)
Love the writeup. Good information. Wondering how common is this mechanism used in other REST apps and and pros and cons?
ReplyDelete