Back Mon 23 Jul, 2012

Google Service Account OAuth2

We've been looking into a number of different Google APIs at work recently, for a bunch of apps that are on the horizon. The first one we wanted to try out was BigQuery, for storing site visitor information. We track site visitors across various sites, and store this data for later analysis. For this purpose, we felt that BigQuery offered a low maintenence, scalable storage and querying facility.

Uploading data into BigQuery is designed to be a done in bulk. We had first assumed that we could stream the data in at, or close to, realtime, but this isn't what it's designed to do. That meant that we would have to temporarily store the data on disk, then have a background task taking care of uploads.

That background worker would need to access the BigQuery API. When using any of Googles APIs, you first need to pass their authentication process. For scenarios such as this, where the API access needs to be granted to a server application rather than an end-user, you have to create a service account. The details of how these are used can be found on the Google Developer portal. In summary, it goes

  1. Set up your service account and download your private key
  2. Create a JSON Web Token (JWT) from your application
  3. Pass the JWT to the OAuth server
  4. Use the access token in the response to invoke your chosen API

The link above lays out all of these steps, but the ones I'll focus on in this post are 2 and 3. At the time of writing, Google Developers doesn't seem to have details on how to create a JWT (pronounced jot) in C#. There is no support for service account authentication in the .NET client libraries that Google provide, though as I understand from Ryan Boyd, Developer Advocate at Google, it is on it's way shortly. In the meantime, here is an example of how we've achieved it.

NB: I'd strongly recommend reading the section on JWTs, so here's the link again. Go read!

The Header

This is just the Base64url endcoded representation of the following JSON {"alg":"RS256","typ":"JWT"}.

var header = new { typ = "JWT", alg = "RS256" };

// encoding
var headerBytes = Encoding.UTF8.GetBytes(_jsonProvider.Serialize(header));
var headerEncoded = _base64Encoder.ToBase64UrlEncodedString(headerBytes);

The Claim Set

This involves a similar process, but the object being encoded here contains details of your service account, the API you are trying to access, and the issue and expiry times.

// jwt claimset
var issueTime = _dateTimeConverter.ToUnixTimeStamp(date);
var expiryTime = _dateTimeConverter.ToUnixTimeStamp(date.AddMinutes(60));

var claim = new
{
iss = serviceAccountEmail,
scope = "https://www.googleapis.com/auth/bigquery",
aud = "https://accounts.google.com/o/oauth2/token",
iat = issueTime,
exp = expiryTime,
};

//encoding
var claimBytes = Encoding.UTF8.GetBytes(_jsonProvider.Serialize(claim));
var claimEncoded = _base64Encoder.ToBase64UrlEncodedString(claimBytes);

A word of caution here. Don't forget to account for Daylight Savings Time when calculating the issue and expiry times. The issue time has to be 'now' (not too far in the past or future), and the expiry has to be after the issue time, up to a maximum of 60 minutes. We wasted a good few hours being scuppered by this as we were on GMT+1 at the time of developing, and we forgot to account for this (DOH!).

The Signature

This is the final part, and involves combining the header and claim set, encoding it, and then signing. This requires using the RSACryptoServiceProvider class from the System.Security.Cryptography namespace.

// signature
var signature = headerEncoded + "." + claimEncoded;
var signatureBytes = Encoding.UTF8.GetBytes(signature);

var certificate = new X509Certificate2(privateKey, privateKeyPassword);

// sign signature using SHA256 with RSA
var rsa = (RSACryptoServiceProvider)certificate.PrivateKey;

// As of .NET 3.5 SP1 the CSP instance works with a provider of type PROV_RSA_AES
var cspParameters = new CspParameters
{
KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
};

var rsaAes = new RSACryptoServiceProvider(cspParameters) { PersistKeyInCsp = false };
var signatureSigned = rsaAes.SignData(signatureBytes, CryptoConfig.MapNameToOID("SHA256"));

var signatureSignedEnc = _base64Encoder.ToBase64UrlEncodedString(signatureSigned);

var jwt = headerEncoded + "." + claimEncoded + "." + signatureSignedEnc;

The final line above is the resulting JWT, which can be sent to the OAuth server for verification.

Submitting the JWT

Submitting the JWT is a simple HTTP POST.

var post = new Dictionary<string, string>
{
{"grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"},
{"assertion", jwt }
};

var content = _httpContentProvider.GetFormUrlEncodedContent(post);
var response = _httpClientProvider.PostAsync(URI, content);
var accessToken = _jsonProvider.Deserialize<AccessToken>(response);

The JSON response is mapped onto an AccessToken class.

public class AccessToken
{
[JsonProperty(PropertyName = "access_token")]
public string Token { get; set; }

[JsonProperty(PropertyName = "token_type")]
public string TokenType { get; set; }

[JsonProperty(PropertyName = "expires_in")]
public int ExpiresIn { get; set; }
}

Once you've received the token, it needs to be stored for the duration of the user session. After the JWT has expired, or the session has ended, the same process needs to be repeated.

Hope you find that useful in some way. Enjoy.