Authenticating with Google Service Account in C# (JWT)

To support scenarios where an unattended application accesses Google data, Google introduced the concept of Service Accounts which allows for unattended log in using JWT (JSON Web Token).

Having fought with the somewhat incomplete documentation and code samples, I decided to summarize and explain the working code here for the benefit of all.

To use the sample code provided here you will need:

  • A Google Account
  • A Google Service Account client e-mail address which is obtained from the Google API Console. The client e-mail address looks like:
    XXX@developer.gserviceaccount.com
  • A Google Service Account private key file (privatekey.p12) which is also obtained from the Google API Console
  • A C# compiler

For simplicity, I created a .NET class called GoogleJsonWebToken with a public static method GetAccessToken which performs the authentication:

public static dynamic GetAccessToken(string clientIdEMail, string keyFilePath, string scope)

The method returns a dictionary with the Google access token accessible by the key “access_token”:

var auth = GoogleJsonWebToken.GetAccessToken(
    clientIdEmail,
    keyFilePath,
    GoogleJsonWebToken.SCOPE_ANALYTICS_READONLY);

Console.WriteLine("Google access token: {0}", auth["access_token"]);

This token can later be returned as part of the standard Google OAuth2 API GetAuthentication delegate result IAuthorizationState:

AuthorizationState result = new AuthorizationState();

result.AccessToken = auth["access_token"];
result.AccessTokenExpirationUtc = DateTime.UtcNow.AddMinutes(30);

return result;

For your convenience I include the full source code of the GoogleJsonWebToken C# class:

using System;
using System.Text;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Web.Script.Serialization;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net;

public class GoogleJsonWebToken
{
    public const string SCOPE_ANALYTICS_READONLY = "https://www.googleapis.com/auth/analytics.readonly";

    public static dynamic GetAccessToken(string clientIdEMail, string keyFilePath, string scope)
    {
        // certificate
        var certificate = new X509Certificate2(keyFilePath, "notasecret");

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

        // claimset
        var times = GetExpiryAndIssueDate();
        var claimset = new
        {
            iss = clientIdEMail,
            scope = scope,
            aud = "https://accounts.google.com/o/oauth2/token",
            iat = times[0],
            exp = times[1],
        };

        JavaScriptSerializer ser = new JavaScriptSerializer();

        // encoded header
        var headerSerialized = ser.Serialize(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = Convert.ToBase64String(headerBytes);

        // encoded claimset
        var claimsetSerialized = ser.Serialize(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = Convert.ToBase64String(claimsetBytes);

        // input
        var input = headerEncoded + "." + claimsetEncoded;
        var inputBytes = Encoding.UTF8.GetBytes(input);

        // signiture
        var rsa = certificate.PrivateKey as RSACryptoServiceProvider;
        var cspParam = new CspParameters
        {
            KeyContainerName = rsa.CspKeyContainerInfo.KeyContainerName,
            KeyNumber = rsa.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
        };
        var aescsp = new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
        var signatureBytes = aescsp.SignData(inputBytes, "SHA256");
        var signatureEncoded = Convert.ToBase64String(signatureBytes);

        // jwt
        var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;

        var client = new WebClient();
        client.Encoding = Encoding.UTF8;
        var uri = "https://accounts.google.com/o/oauth2/token";
        var content = new NameValueCollection();

        content["assertion"] = jwt;
        content["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer";

        string response = Encoding.UTF8.GetString(client.UploadValues(uri, "POST", content));

        var result = ser.Deserialize<dynamic>(response);

        return result;
    }

    private static int[] GetExpiryAndIssueDate()
    {
        var utc0 = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
        var issueTime = DateTime.UtcNow;

        var iat = (int)issueTime.Subtract(utc0).TotalSeconds;
        var exp = (int)issueTime.AddMinutes(55).Subtract(utc0).TotalSeconds;

        return new[] { iat, exp };
    }
}