Thursday, 5 September 2013

Digitally Signing a Google API V3 Service Request URL Using C#

Overview

If you have a Business account for increased volumes of traffic, you have to sign the request URL using your private key, rather than using an API key which was required for V2.

This article details what is required and has an example of how to implement signing using Python.

The thing which tripped me up was this:

Note: Modified Base64 for URLs replaces the + and / characters of standard Base64 with - and _ respectively, so that these Base64 signatures no longer need to be URL-encoded.”

Our key had ‘_’ characters in which needed swapping to ‘/’ in order to convert to Base64 bytes and many of the generated hashes had ‘+’ and ‘/’ characters which needed converting to ‘-‘ and ‘_’.

String Formats

The following constants are the string formats I used for the full URL and the portion of the URL for signing:

private const string SEARCH_URL = "http://maps.googleapis.com/maps/api/geocode/json?latlng={0},{1}&sensor=false&client={2}&signature={3}";
private const string SIGN_URL = "/maps/api/geocode/json?latlng={0},{1}&sensor=false&client={2}";

CreateSignature Method

This method creates a signature for the URL with the private key:

private string CreateSignature(string url, string privateKey)
{    
  // Replace file-safe characters
  byte[] key = Convert.FromBase64String(privateKey.Replace('-', '+').Replace('_', '/'));
  byte[] buffer = Encoding.UTF8.GetBytes(url);
  byte[] hash = null;

  // Initialize the keyed hash object.
  using (HMACSHA1 hmac = new HMACSHA1(key))
  {
    // Create hash
    hash = hmac.ComputeHash(buffer);
  }

  // Replace unsafe characters
  return Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_');
}

Implementation

It can be implemented in the following example which returns an address for a lat/long position (Position and Address are basic POCO objects):

private string _privateKey = "My_Private_Key=";
private string _clientId = "My-ClientId";   

public async Task<Address> GetAddress(Position position)
{
  string url = string.Format(SIGN_URL, position.Latitude, position.Longitude, _clientId);

  string sig = this.CreateSignature(url, this._privateKey);

  string search = string.Format(SEARCH_URL, position.Latitude, position.Longitude, _clientId, sig);
  string data = null;

  try
  {
    var req = WebRequest.CreateHttp(search);
    var res = await req.GetResponseAsync();

    using (Stream s = res.GetResponseStream())
    using (StreamReader str = new StreamReader(s))
    {
      data = await str.ReadToEndAsync();
    }
  }
  catch (WebException wex)
  {
    throw new Exception("Web server request failed", wex);
  }

  if (data != null)
  {
    var address = this.ParseAddressJson(data);

    return address;
  }

  return null;
}

I’ve not included the JSON parsing routine, but that bit’s fairly easy to implement.

If something is wrong with the signature you will get a 403 code.