JSON Web Token (JWT) for Google Cloud Platform in Unity
Sep, 2016
4 minutes read

Lately I have been working on a Unity Application which pulls content from Google Cloud Storage.

Dealing with Google Cloud Platform requires you to authenticate with an access token you receive from their servers. You get one by sending a JSON Web Token (JWT) which contains your service account’s data.

It does sound (it is) complicate. There is an official Google Cloud APIs library for .NET which makes your life easier, but sadly it requires .NET framework 4.5, and therefore is not compatible with Mono / Unity.

Several people online have rolled their own C# implementation. None of their solutions worked out of the box for me, but after re-arranging some pieces and finding out several way it didn’t work I managed to conjure up a method which seems to work fine with the latest APIs as of the current date (October 2016). Still, kudos to Levikton from this StackOverflow thread and this post on zavitax’s blog.

You will need the Newtonsoft.Json C# extension, which is handily available as a UnityPackage.

using System;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Collections.Generic;

using Newtonsoft.Json;
using UnityEngine;
using Newtonsoft.Json.Linq;

public class GoogleJsonWebToken
{
    public const string SCOPE_READONLY = 
        "https://www.googleapis.com/auth/devstorage.read_only";

    public static string GetJwt(
        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://www.googleapis.com/oauth2/v4/token",
            iat = times[0],
            exp = times[1],
        };

        // encoded header
        var headerSerialized = JsonConvert.SerializeObject(header);
        var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
        var headerEncoded = Base64UrlEncode(headerBytes);

        // encoded claimset
        var claimsetSerialized = JsonConvert.SerializeObject(claimset);
        var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
        var claimsetEncoded = Base64UrlEncode(claimsetBytes);

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

        // signature
        RSACryptoServiceProvider rsa = 
            (RSACryptoServiceProvider)certificate.PrivateKey;
        var signatureBytes = rsa.SignData(inputBytes, "SHA256");
        var signatureEncoded = Base64UrlEncode(signatureBytes);

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

        return jwt;
    }

    public static WWW GetAccessTokenRequest(string jwt)
    {
        string url = "https://www.googleapis.com/oauth2/v4/token";
        WWWForm form = new WWWForm();
        form.AddField(
            "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
        form.AddField("assertion", jwt);
        Dictionary<string, string> headers = form.headers;
        headers["Content-Type"] = "application/x-www-form-urlencoded";

        WWW www = new WWW(url, form.data, headers);
        return www;
    }

    // from JWT spec
    private static string Base64UrlEncode(byte[] input)
    {
        var output = Convert.ToBase64String(input);
        output = output.Split('=')[0]; // Remove any trailing '='s
        output = output.Replace('+', '-'); // 62nd char of encoding
        output = output.Replace('/', '_'); // 63rd char of encoding
        return output;
    }

    // from JWT spec
    private static byte[] Base64UrlDecode(string input)
    {
        var output = input;
        output = output.Replace('-', '+'); // 62nd char of encoding
        output = output.Replace('_', '/'); // 63rd char of encoding
        switch (output.Length % 4) // Pad with trailing '='s
        {
            case 0: break; // No pad chars in this case
            case 2: output += "=="; break; // Two pad chars
            case 3: output += "="; break; // One pad char
            default: throw new System.Exception("Illegal base64url string!");
        }
        var converted = Convert.FromBase64String(output); // Standard base64 decoder
        return converted;
    }

    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 };
    }
}

Via this helper class you can assemble the data required for the JWT, and fire up an HTTP request to Goggle’s server to get back the authentication token.

I used devstorage.read_only as a scope since I had to access files on the cloud storage, but you can use a different scope based on your needs. More about scopes here.

Here’s a practical example on how to use it:

IEnumerator Start()
{
    string email = "your-email@your-project.iam.gserviceaccount.com";
    string cert = "path-to-your-certificate.p12"
    string jwt = GoogleJsonWebToken.GetJwt(
        email, cert, GoogleJsonWebToken.SCOPE_READONLY);
    WWW tokenRequest = GoogleJsonWebToken.GetAccessTokenRequest(jwt);
    yield return tokenRequest;

    if (tokenRequest.error == null)
    {
        var deserializedResponse = JSON.Parse(tokenRequest.text);
        var token = deserializedResponse["access_token"];
        Debug.Log("Access Token Obtained: " + token);
    }
    else
    {
        Debug.Log("ERROR: " + tokenRequest.text);
    }
}

Hope this will be of use to someone!


Back to posts


comments powered by Disqus