REST APIs that serve their content via HTTP have limited options for authentication. One of these is request signing. With a little bit of custom specification the standard approach HMAC can be utilized for efficient and secure authentication.
Inmy last blog entry I explained an approach for using API Keys in a REST API. This approach has a significant drawback, which comes from the necessity to transmit the authentication token with every request. REST APIs that only use HTTPS certainly can live with that, but many can’t guarantee encryption in the transport layer.
Overview of HMAC
Today I want to dive a little bit into authentication with Keyed-Hash Message Authentication Code (HMAC). With HMAC, the server and the client share a secret access key . The access key happily lives in the respective data bases and is never transmitted across a line. Instead, the key is used to generate a hash for signing the message contents.
The access key consists of
- access key ID : a unique string for the identification of the access key. This string should be safe for URL encoding, e.g. hexadecimal or alphanumerical and is stored in plaintext in the database.
- secret key : A string that is used for message signing. It should be stored in an encrypted form in the database.
- user ID (optional): A user identification which this key belongs to.
The general process consists for authentication via HMAC of these steps:
- constructs a request
- calculates a hash for the request using the secret key
- transmits the request including the access key ID and the hash
The server then
- looks up the access key in the data base
- calculates the hash for the received request using the secret key
- compares the received hash value with the one that was locally computed. Authorization was successful if the hashes match.
- checks the request date. If it is no older than a given number of minutes, the request is served.
The hash and access key ID can be transmitted in a special header, e.g. com.eclipsesource.auth-key-signature and com.eclipsesource.auth-hash-sha256 by following the updated naming conventions for application protocol headers from RFC-6648 .
Obviously the specification for the hash calculation must be precise when different implementations on the server and the client are expected. Here’s an example:
com.eclipsesource.auth-hash-sha256 = AccessKeyId + ":" + Signature Signature = Base64( HMAC-SHA256( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ); StringToSign = HTTP-Verb + "\n" + Content-Type + "\n" + CanonicalizedResource + "\n" + CanonicalizedApplicationHeaders + CanonicalizedFormParameters CanonicalizedResource = <HTTP-Request-URI Path> CanocalizedApplicationHeaders = <in lexicographical sort order, lines of: > [ CanonicalizedApplicationHeader + "\n" ] CanonicalizedApplicationHeader = HeaderName + ":" + HeaderValue + "\n" CanonicalizedFormParameters = <in lexicographical sort order, lines of:> [ CanonicalizedFormParameter + "\n" ] CanonicalizedFormParameter = ParameterName + ":" + ParameterValue
So, a GET request to the URL http://eclipsesource.com/myrestapi/myresource would have a StringToSign value of
GET\n \n /myrestapi/myresource\n com.eclipsesource.request-date:2016-07-06T04:59:52Z;\n com.eclipsesource.auth-key-id:\n
The line for the content type is empty, because this request does not transport any files. For the case of file transfers it is sensible to specify an application header that also transmits a hash value for the file, e.g. com.eclipsesource.content-sha256 to avoid usage of the HTTP Header Content-MD5 .
The request should contain a date, which is done here with the application header com.eclipsesource.request-date . Following the date format specification from RFC3339 is a good compromise for human readability both in plain text and url encoded format.
The hash value is excluded from the StringToSign.
The request date check is included in this authentication method to mitigate capture-and-replay attacks. To improve security further, HTTPS is the way to go.
Calculation of the hash
HMAC has been around for a while, and virtually every language has support for computing HMAC values with different algorithms. Here is a simple way to do the calculation in Java utilizing the Apache Commons Codec library .
byte hmacSha256 = org.apache.commons.codec.digest.HmacUtils.hmacSha256(secretAccessKey, message); String hmacSha256Base64 = Base64.getEncoder().encodeToString(hmacSha256);
HMAC is a standard approach for authentication over insecure transport layers. With these hints given it should be straightforward to define your own authentication layer that is both efficient and secure.