Implementing AWS authentication for your own REST API

September 07, 2011 - authentication aws rest

If you need to build an authentication mechanism for an HTTP-based REST API, a common approach is to use HTTP Basic - it's simple, all clients have it built-in, it's easy to test from the browser, and you can store passwords as hashes. The downside is that your credentials are transmitted in (nearly) plain text, which makes SSL (with its associated security restrictions and computational cost) a necessity.

If you'd like to implement a simple scheme for a non-sensitive API that doesn't require SSL, this is more complicated. HTTP Digest requires storing unhashed passwords on the server, and requires a challenge-response conversation between server & client. Schemes like Kerberos and three-legged oath require hanging your hat on a third-party authentication provider, and are awkward to implement in a client.

Luckily, in software if you hit a problem you can usually copy somebody else's solution. This is what the Microsoft Azure team did when implementing their API authentication - "Let's just copy what Amazon does". Amazon probably copied someone else. Who am I to argue with that approach?

The general concept behind these schemes is relatively simple:

  1. Come up with a way to generate API & secret keys for a client. These are usually base64-encoded cryptographically generated random byte arrays.
  2. For each request, hash a string containing the requested URL and specific headers (including the Date header) with the secret key using HMAC-SHA1.
  3. Add an Authorize header with a custom scheme name (e.g. 'AWS'), containing the access key & base64-encoded signature separated by a colon.

The client goes through this process to generate the Authorize header, then the server performs a reverse of the procedure using the stored secret key to authenticate the request. Additionally, the server checks the Date header value against server time to check for replay attacks.

Below is the source for an AuthorizeAttribute subclass (for an MVC3 REST API). The code is easily adaptable to other frameworks, such as an OpenRasta PipelineContributor. The injected Session property in this instance is an NHibernate Session, and the ApiKey class is mapped to a database table. Successful authentication adds a custom IPrincipal to the HttpContext. Note that none of the x-aws headers are being used.

public class ApiAuthenticateAttribute : AuthorizeAttribute
{    
    private static System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding();
 
    [Inject]
    public ISession Session { private get; set; }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var request = filterContext.HttpContext.Request;
        IPrincipal principal = null;

        if (request.Headers["Authorization"] != null && request.Headers["Authorization"].StartsWith("AWS "))
        {
            // Amazon AWS authentication scheme.
            var credential = filterContext.HttpContext.Request.Headers["Authorization"].Substring(4).Split(':');
            var apiKey = Session.Query<ApiKey>().Where(k => k.AccessKey == credential[0]).FirstOrDefault();
            if (apiKey != null && !apiKey.IsDisabled && credential.Count() > 1)
            {
                // check the date header is present & within 15 mins
                DateTime clientDate;
                if (request.Headers["Date"] != null
                    && DateTime.TryParseExact(request.Headers["Date"], "R", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal, out clientDate)
                    && Math.Abs((clientDate - DateTime.UtcNow).TotalMinutes) <= 15)
                {
                    // build the signature & check for match
                    var stringToSign = String.Format("{0}\n{1}\n{2}\n{3}\n{4}",
                        request.HttpMethod,
                        request.Headers["Content-MD5"] ?? "",
                        request.Headers["Content-Type"] ?? "",
                        request.Headers["Date"] ?? "",
                        request.RawUrl);

                    var hmac = new HMACSHA1(utf8.GetBytes(apiKey.SecretKey));
                    var signature = Convert.ToBase64String(hmac.ComputeHash(utf8.GetBytes(stringToSign)));
                    if (signature == credential[1])
                    {
                        principal = apiKey.ToPrincipal();
                    }
                }
            }
        }

        if (principal == null)
        {
             filterContext.Result = new HttpUnauthorizedResult();
        }
        else
        {
            filterContext.HttpContext.User = principal;
        }
    }
}

Generating API Keys can be done like so:

public ApiKey()
{
    // Generate random keys by using RNGCryptoServiceProvider & Base64 encoding the output
    // Key lengths match AWS keys.
    var rngProvider = RNGCryptoServiceProvider.Create();
    var bytes = new byte[15];
    rngProvider.GetBytes(bytes);
    // Do some magic to ensure we have uppercase & digits only.
    AccessKey = Convert.ToBase64String(bytes).ToUpper().Replace("+", "0").Replace("/", "9");
    bytes = new byte[30];
    rngProvider.GetBytes(bytes);
    SecretKey = Convert.ToBase64String(bytes);
}

For the client, most languages have freely available Amazon client code that can be easily adapted. Reusing a popular scheme like this saves a lot of time & energy over rolling a completely custom solution, particularly where a number of disparate client platforms are likely to be used.