fredag, marts 08, 2013

Using Ramone for OAuth2 authorization with Google APIs

I have blogged a couple of times about Ramone which is a C# client side library for interacting with RESTful web APIs (see https://github.com/JornWildt/Ramone). This time I will show how to use Ramone to authorize with Google's web services using OAuth2.

The OAuth2 flow to use is a variation of the "Client Credentials Grant" flow from OAuth2 (see http://tools.ietf.org/html/rfc6749#section-4.4) - but instead of sending the client credentials in clear text over the wire it uses a signed JSON Web Token (JWT) assertion instead (see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-06).

The concept itself is relatively simple: create a digitally signed piece of data (an assertion) that proves who the client is and then present that assertion to Google. It does although require quite a few steps to get it right.

Before we start making HTTP requests we must first acquire a set of client credentials from Google. This comes in the form of a X509 certificate which contains the private signing key for the client. To obtain this certificate we use Googles API console at https://code.google.com/apis/console. Login and go to "API access" where you click on the "Create client ID" button and select "Service account":



When your client credentials has been created you are asked to save them somewhere secure. Save them in a place where your program can load it from later.


The next step is to get Google's Token Endpoint URL. At the time of writing it is https://accounts.google.com/o/oauth2/token but this may of course change in the future.

Now we are ready to rock! First we initialize Ramone with the Google stuff:

using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Ramone;
using Ramone.OAuth2;
using Ramone.Utility.JsonWebToken;

const string GoogleAPIBaseUrl = "https://www.googleapis.com/oauth2/v1";
const string TokenEndpointUrl = "https://accounts.google.com/o/oauth2/token";

ISession Session = RamoneConfiguration.NewSession(new Uri(GoogleAPIBaseUrl));

OAuth2Settings settings = new OAuth2Settings
{
  TokenEndpoint = new Uri(TokenEndpointUrl),
  ClientAuthenticationMethod = OAuth2Settings.DefaultClientAuthenticationMethods.Other
};

Session.OAuth2_Configure(settings);
Then we load the certificate we got from Google. That is albeit not as easy as it sounds since the default X509 certificate doesn't support the RSA256 signing required by the standard. So we have to load the certificate first and then create a new RSACryptoServiceProvider from that:

private static RSACryptoServiceProvider GetCryptoServiceProvider()
{
  const string CertificatePath = "path-to-your-certificate-file";

  X509Certificate2 certificate = new X509Certificate2(CertificatePath, "notasecret", X509KeyStorageFlags.Exportable);

  using (RSACryptoServiceProvider cp = (RSACryptoServiceProvider)certificate.PrivateKey)
  {
    // Create new crypto service provider that supports SHA256 (and don't ask me why the first one doesn't)
    CspParameters cspParam = new CspParameters
    {
      KeyContainerName = cp.CspKeyContainerInfo.KeyContainerName,
      KeyNumber = cp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2
    };

    return new RSACryptoServiceProvider(cspParam) { PersistKeyInCsp = false };
  }
}
At last we can authenticate with Google using the client credentials we loaded from the certificate. The "Issuer" value below is the e-mail address of your "Service account" client as stated in Google's API console:

const string Issuer = "your-client-issuer-email-address-from-google";
const string Scope = "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile";

using (RSACryptoServiceProvider csp = GetCryptoServiceProvider())
{
  AssertionArgs args = new AssertionArgs
  {
    Audience = TokenEndpointUrl,
    Issuer = Issuer,
    Scope = Scope
  };

  Session.OAuth2_GetAccessTokenFromJWT_RSASHA256(csp, args);
}
The last statement makes the actual request to Google with the signed JWT assertion, reads the returned OAuth2 access token and stores it in the session for use in the following requests.

If this works you are done. Ramone has authorized with Google using your client credentials without any intervention from a user.

To prove that it works we can now fetch the user name and e-mail of the current user (your client) identified by the access token returned from Google:

Console.WriteLine("Reading user information from Google");
using (var response = Session.Bind("userinfo").AcceptJson().Get<dynamic>())
{
  var body = response.Body;
  Console.WriteLine("\nRESULT:");
  Console.WriteLine("User name: " + body.name);
  Console.WriteLine("E-mail: " + body.email);
}
Have fun :-)