Json Web Token (JWT) for Google Cloud Platform in Unity
Json Web Token (JWT) for Google Cloud Platform in Unity
Written on
Sep 29, 2016
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!