We previously looked at how you can use the WebAuthenticationBroker to allow end users to authorize a UWP app to access their Google account information. In this post we’ll complete the sample by describing how to authorize a UWP app to access Google Cloud Product services.
To demonstrate this, we’ll recreate the ASP.NET MVC sample that logins in a user, grabs their profile image, and uploads that image to Google Storage.
However, just like in our previous sample, we don’t have any helper libraries we can use, because as of this writing the Google Cloud SDK does not yet natively support UWP. Fortunately, we can always access the services via REST, so let’s get to it.
Authorization Workflow
Just like in our MVC sample, we need a way to authorize our app to access the services in our GCP account so that it can make the appropriate changes, in this case, uploading an image to Google Storage.
However, unlike our MVC sample, we are unable to use the same Web Server OAuth Workflow because it would require that we issue a POST containing the client secret, and this request could be hijacked by a malicious user, compromising our security.
Instead, we need to use a different OAuth workflow for Service Accounts which uses a JSON Web Token (JWT) to authorize the request. The JWT contains the scope of your request, and needs to be signed with your private key, which is issued when you create credentials in the Google API Console.
Recall that when you create a Service Account in the console (in this case we want to use the option for P12), you are immediately served a file containing your private key.
This is the only time that key is available, so be sure to keep that key safe, or you will have to delete it and create a new one.
Once you’ve created the JWT, having signed it with your private key, you simply POST this request to the Google authorization endpoint, and you will be returned a response containing a Bearer token that you can use to make authorized requests to the scopes you requested in the JWT.
Before we look at the code, let’s take a moment to review an important consideration regarding security for your app.
Security Considerations
Because the request needs to be signed, your app must have access to the file containing your private key, in this case the P12 file issued when you created the Service Account you’ll be using to authenticate your app.
Although you can simply include this file in your app, you probably don’t want to do this for any scenario other than testing locally. Distributing your app with the key file means anyone could use it to sign requests on behalf of your app!
A slightly better idea may be to embed the file as a resource into your app, so that it’s not freely available as a separate file. This is what we’re doing in this example.
However, a malicious user could certainly still decompile your app and retrieve the key that way. Encryption and obfuscation is probably a good idea, especially in specific scenarios, such as internal business apps, where you can control access to devices where your app is deployed.
Ideally, however, you probably want to setup a proxy web service to handle requests to the GCP service, and use the server authentication from our previous example to perform the GCP service operations. This way your server can be authorized entirely outside your app, protecting your key from being stolen and giving you full control of your apps content workflow.
That being said, I am not a security expert by any means, and would love to hear thoughts from how to best protect a UWP app from others in the comments. This sample is meant only to serve as a simple example of accessing GCP in a UWP app, so please use with care.
Now that we understand the workflow and security needs, let’s dive into the code!
Creating a JWT in UWP Apps
The trickiest part of putting this sample together was definitely creating the JWT token, because the token has to be signed, and cryptography isn’t something I personally know a lot about.
Fortunately, I found a helpful sample from developer Ilya Melamed who wrote a post about Authenticating with Google Service Account in C# (JWT). This sample is for the full .NET framework (such as WPF apps) and many of the classes used are not available for UWP. However, it was a useful starting point which I used to expand to UWP.
X509Certificate2
The key part of Ilya’s sample is the use of the X509Certificate2 class which can load the P12 key file from Google. Although UWP does not have this class natively in the framework, there is a nuget package available which adds these classes: System.Security.Cryptography.X509Certificates
With this we’ll be able to open the P12 key and use it to sign our JWT, so in my sample project I’ve added that key file as an embedded resource:
To open the file we extract it from the assembly as a stream, and read the contents as bytes:
var assembly = this.GetType().GetTypeInfo().Assembly;
var stream = assembly.GetManifestResourceStream("GoogleUwpTestApp.Falafel-Test-Project-232e85f0df81.p12");
using (var streamReader = new BinaryReader(stream)) {
var bytes = streamReader.ReadBytes((int) stream.Length);
// ...
}
This will eventually be passed to the X509Certificate2 constructor, which accepts those bytes and the password, which is “notasecret” for GCP certificates:
var certificate = new X509Certificate2(certificateBytes, "notasecret", X509KeyStorageFlags.Exportable);
Now that we have a way to sign our token, let’s create it!
Creating the Token
The header is the same for all requests, indicating the type and signing algorithm:
var header = new { typ = "JWT", alg = "RS256" };
Next we need to create the claimset, which contains our client ID, requested scopes, and other details described in the Google OAuth documentation:
var times = GetExpiryAndIssueDate();
var claimset = new{ iss = clientID, scope = scope, aud = "https://www.googleapis.com/oauth2/v4/token", iat = times[0], exp = times[1],};
These components of the JWT then need to be Base64 encoded:
// encoded header
var headerSerialized = JsonConvert.SerializeObject(header);
var headerBytes = Encoding.UTF8.GetBytes(headerSerialized);
var headerEncoded = Convert.ToBase64String(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = Convert.ToBase64String(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
And finally, we use the certificate we opened to sign the token, putting it all together to get the final JWT:
// signature
string signatureEncoded;
try {
var signatureBytes = certificate.GetRSAPrivateKey().SignData(inputBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); signatureEncoded = Convert.ToBase64String(signatureBytes);
} catch (Exception ex) {
signatureEncoded = string.Empty; }
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;
Now that we have our token, we can issue a POST to the Google authorization endpoint:
// wrap parameters in a Form object
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("assertion", jwt), new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
});
// the url to send the POST request (from the google docs)
var postUrl = "https://www.googleapis.com/oauth2/v4/token";
// submit the request
var client = new HttpClient();
var result = await client.PostAsync(postUrl, content);
var str = await result.Content.ReadAsStringAsync();
dynamic parsedResult = JsonConvert.DeserializeObject(str);
return result == null ? string.Empty : parsedResult.access_token;
And if we did everything right, we’ll get back the access token for our app, which we’ll use to authorize subsequent requests to our GCP services.
Here’s the complete GoogleJsonWebTokenHelper class for UWP:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
namespace GoogleUwpTestApp.Google{
public class GoogleJsonWebTokenHelper {
private readonly string clientID;
private readonly string scope;
public GoogleJsonWebTokenHelper(string clientID, string scope) {
this.clientID = clientID;
this.scope = scope;
}
public async Task<string> GetAccessToken(string certificateFilePath) {
// certificate
var certificate = new X509Certificate2(certificateFilePath, "notasecret", X509KeyStorageFlags.Exportable);
return await GetAccessTokenInternal(certificate);
}
public async Task<string> GetAccessToken(byte[] certificateBytes) {
// certificate
var certificate = new X509Certificate2(certificateBytes, "notasecret", X509KeyStorageFlags.Exportable);
return await GetAccessTokenInternal(certificate);
}
private async Task<string> GetAccessTokenInternal(X509Certificate2 certificate) {
// header
var header = new { typ = "JWT", alg = "RS256" };
// claimset
var times = GetExpiryAndIssueDate();
var claimset = new {
iss = clientID,
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 = Convert.ToBase64String(headerBytes);
// encoded claimset
var claimsetSerialized = JsonConvert.SerializeObject(claimset);
var claimsetBytes = Encoding.UTF8.GetBytes(claimsetSerialized);
var claimsetEncoded = Convert.ToBase64String(claimsetBytes);
// input
var input = headerEncoded + "." + claimsetEncoded;
var inputBytes = Encoding.UTF8.GetBytes(input);
// signature
string signatureEncoded;
try {
var signatureBytes = certificate.GetRSAPrivateKey().SignData(inputBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
signatureEncoded = Convert.ToBase64String(signatureBytes);
} catch (Exception ex) {
signatureEncoded = string.Empty;
}
// jwt
var jwt = headerEncoded + "." + claimsetEncoded + "." + signatureEncoded;
// wrap parameters in a Form object
var content = new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("assertion", jwt), new KeyValuePair<string, string>("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
});
// the url to send the POST request (from the google docs)
var postUrl = "https://www.googleapis.com/oauth2/v4/token";
// submit the request
var client = new HttpClient();
var result = await client.PostAsync(postUrl, content);
var str = await result.Content.ReadAsStringAsync();
dynamic parsedResult = JsonConvert.DeserializeObject(str); return result == null ? string.Empty : parsedResult.access_token;
}
private 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 };
}
}
}
Uploading an Image
Now that we have the token we can proceed to issue a simple POST request with the image contents we retrieve from the user, including the token in the header:
public async Task UploadImage(string url, string filename, string bucketName) {
var client = new HttpClient();
var resultStream = await client.GetStreamAsync(url);
Byte[] bytes;
using (var memoryStream = new MemoryStream()) {
resultStream.CopyTo(memoryStream);
bytes = memoryStream.ToArray();
}
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + access_token);
var uploadUri = string.Format("https://www.googleapis.com/upload/storage/v1/b/{0}/o?uploadType=media&name={1}&predefinedAcl=publicRead", bucketName, filename);
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uploadUri);
request.Content = new ByteArrayContent(bytes);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
request.Content.Headers.ContentLength = bytes.Length;
HttpResponseMessage response = await client.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
}
The result is an image uploaded to the storage bucket, which we use to refresh the list, and now have a new profile image added to the ListView:
Retrieving the items from Google storage is done similarly to our MVC sample; we simply query the rest API, sending the same bearer token we already retrieved:
public async Task<StorageModel> GetStorageImages(string bucketName) {
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + access_token);
var bucketUri = string.Format("https://www.googleapis.com/storage/v1/b/{0}/o", "falafel-test-profile-images");
var resultJson = await client.GetStringAsync(bucketUri);
var storageResult = JsonConvert.DeserializeObject<StorageModel>(resultJson);
return storageResult;
}
In this case, StorageModel is just a simple C# class that encapsulates the JSON result from the call:
public class Item {
public string kind { get; set; }
public string id { get; set; }
public string selfLink { get; set; }
public string name { get; set; }
public string bucket { get; set; }
public string generation { get; set; }
public string metageneration { get; set; }
public string contentType { get; set; }
public string timeCreated { get; set; }
public string updated { get; set; }
public string storageClass { get; set; }
public string timeStorageClassUpdated { get; set; }
public string size { get; set; }
public string md5Hash { get; set; }
public string mediaLink { get; set; }
public string crc32c { get; set; }
public string etag { get; set; } }
public class StorageModel {
public string kind { get; set; }
public List<Item> items { get; set; }
}
That’s all there is to it!
Wrapping Up
Although the Google Cloud SDK doesn’t yet have native support for UWP apps, it is still possible to perform any of the various operations for both Service and User accounts using plain old REST commands. Authorizing your app to access GCP Services via Service Account takes a bit more work, and you do want to be careful with your certificates, but it’s absolutely possible with a little extra work.
Hopefully we’ll start to see some UWP support in the coming months, but in the meantime, as always I hope this was helpful, and thanks for reading!