16

The short question is whether is this possible and if so, how?

Outline

I have a .NET application which currently uses a service account to access information across a Google Apps domain using the Google Drive API. This works fine using the google-api-dotnet-client library and code along the same lines as shown in the samples here - which are currently a very good basic example of what I'm doing.

What I want to do now is extend it so as well as using those APIs provided by the "new" google-api-dotnet-client library, it uses the older "GData" libraries, as provided for via the older google-gdata library, specifically the Spreadsheets API (and perhaps more to come).

The Problem

This is where the difficulty arises. The former library does exactly what I want, as evidenced by the second link in the first paragraph above - and the fact I have it doing it myself. HOWEVER... although the second library has been updated to support OAuth 2.0 in addition to OAuth 1.0 and the other older auth techniques, it does not - as far as I can tell from extensive Googling and trail-and-error - allow the "service account on behalf of all my users" operation which I need.

My question is whether I'm missing something (possibly a hard to find or undocumented something) which would allow me to do what I want. Failing that, is there any way I could force this behaviour and make these two libraries operate side by side?

The ideal solution

Ideally I would love some way of having the Google.GData.Spreadsheets.SpreadsheetsService instance be able to take advantage of the Google.Apis.Authentication.Auth2Authenticator<AssertionFlowClient> instance I'm already using... somehow. Is such witchcraft possible? I'm I missing the obvious?

Failing that, I'm happy to do the whole OAuth2 "assertion flow client" dance again if I have to, in some way that the older library can handle.

Help?

Other Thoughts

I have considered - and rejected for the time being - the option of starting from scratch and writing my own library to make this happen. This is for two reasons:

  1. The gdata library already exists, and has been developed by many people likely cleverer than myself. I'm not so arrogant that I believe I can do better.
  2. I'm not certain the OAuth2 with service account approach is even supported/allowed on these older APIs.

An alternate approach which I've been hoping to avoid but may have to fall back to depending on the answers here will be to use 2-legged OAuth 1.0 for portions of this. I'd prefer not to, as having parts of the app rely on one old auth method whilst other parts do it the nice new way just feels wrong to me. And there's that much more to go wrong...


Updates

I have considered the possibility of subclassing GDataRequestFactory and GDataRequest so I can make my own request factory and have that take the instance of Google.Apis.Authentication.Auth2Authenticator<AssertionFlowClient> (well, an instance of Google.Apis.Authentication.IAuthenticator anyway) which could step in to authenticate the request just before it's called. However... the constructor for GDataRequest is internal, which has stopped me.

It's really looking like this isn't meant to be.

Mark Embling
  • 12,605
  • 8
  • 39
  • 53

3 Answers3

20

For the sake of other folks coming across this question (now that the solution linked to in the accepted answer uses deprecated code), here's how I solved it:

First, start in "new API" land (use the Google.Apis.Auth nuget package) by setting up a ServiceAccountCredential following Google's Service Account example:

//In the old api, this accessed the main api accounts' sheets, not anymore
//** Important ** share spreadsheets with the Service Account by inviting the "serviceAccountEmail" address to the sheet
string serviceAccountEmail = "12345697-abcdefghijklmnop@developer.gserviceaccount.com";

var certificate = new X509Certificate2(@"key.p12", "notasecret", X509KeyStorageFlags.Exportable);

ServiceAccountCredential credential = new ServiceAccountCredential(
   new ServiceAccountCredential.Initializer(serviceAccountEmail)
   {
       Scopes = new[] { "https://spreadsheets.google.com/feeds", "https://docs.google.com/feeds" }
   }.FromCertificate(certificate));

Tell the credential to request an Access Token:

credential.RequestAccessTokenAsync(System.Threading.CancellationToken.None).Wait();

Now it's time to switch back to "old API" land (use the Google.GData.Spreadsheets nuget package). Start by constructing the SpreadsheetsService (similar to Google's example):

SpreadsheetsService service = new SpreadsheetsService("MySpreadsheetIntegration-v1");

To use Service Account authentication, we'll create an instance of the GDataRequestFactory and set a custom Authorization header:

var requestFactory = new GDataRequestFactory("My App User Agent");
requestFactory.CustomHeaders.Add(string.Format("Authorization: Bearer {0}", credential.Token.AccessToken));

Finally, set the SpreadsheetsService's RequestFactory property to this new factory:

service.RequestFactory = requestFactory;

And go ahead and use the SpreadsheetsService as you would had you authenticated using any other technique. (Tip: share spreadsheets with the Service Account by inviting the serviceAccountEmail address to the sheet)

TheSoftwareJedi
  • 34,421
  • 21
  • 109
  • 151
Matt K
  • 438
  • 3
  • 9
  • Have updated the accepted answer to this one since it is now the best (only) way to accomplish this, and it works just as well as my original solution whilst using the new libraries. Worth noting though that the scope needs to be set appropriately for the service(s) used :-). – Mark Embling May 22 '15 at 14:14
  • Thank for this answer, helped point me in the right direction. For anyone else adapting this to access the EmailSettings API, you need to add a 'User' property to the .Initializer with an admins primary email address. – WhoIsRich Jul 06 '15 at 21:49
4

I managed to solve this by subclassing GDataRequestFactory and creating my own implementation of the interfaces implemented by GDataRequest. This implementation wraps an instance of GDataRequest instantiated via reflection, and adds in the necessary code to perform authentication using an instance of IAuthenticator (in my case, Auth2Authenticator).

I wrote a blog post on it and added an example as a Gist:

Feel free to use this if it helps you (BSD licence).

Mark Embling
  • 12,605
  • 8
  • 39
  • 53
  • 1
    That is some excellent code. Unfortunately since then Google has marked IAuthenticator obsolete and uses ServiceAccountCredential. It appears to be a completely different mechanism than before so I couldn't use your method to make an adapter. Hopefully they'll release a spreadsheet compatible API sometime! – JonH Aug 19 '14 at 03:48
  • ***IAuthenticator*** not available now ? – PreguntonCojoneroCabrón Jul 19 '15 at 09:56
3

Hey just stumbled accross the same problem and produced a different solution:

Has anybody ever concidered of writing the parameters from the credentials-object directly to an OAuth2Parameters-Object?

I did this and it worked nicely:

public class OAuthTest
{  
    OAuth2Parameters param = new OAuth2Parameters();

    public OAuthTest()
    {
        Debug.WriteLine("Calling: AuthGoogleDataInterface()");
        bool init = AuthGoogleDataInterface();
        if (init)
        {
            GOAuth2RequestFactory requestFactory = new GOAuth2RequestFactory(null, "My App User Agent", this.param);
            //requestFactory.CustomHeaders.Add(string.Format("Authorization: Bearer {0}", credential.Token.AccessToken));
            var service = new SpreadsheetsService("MyService");
            service.RequestFactory = requestFactory;
            SpreadsheetQuery query = new SpreadsheetQuery();

            // Make a request to the API and get all spreadsheets.
            SpreadsheetFeed feed = service.Query(query);

            // Iterate through all of the spreadsheets returned
            foreach (SpreadsheetEntry entry in feed.Entries)
            {
                // Print the title of this spreadsheet to the screen
                Debug.WriteLine(entry.Title.Text);
            }
        }
        Debug.WriteLine(m_Init);
    }

    private bool AuthGoogleDataInterface()
    {
        bool b_success;
        try
        {
            Console.WriteLine("New User Credential");
            // New User Credential
            UserCredential credential;
            using (var stream = new FileStream("client_secrets.json", FileMode.Open, FileAccess.Read))
            {
                GoogleClientSecrets GCSecrets = GoogleClientSecrets.Load(stream);
                string[] ArrScope = new[] { "https://spreadsheets.google.com/feeds", "https://docs.google.com/feeds" };
                credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GCSecrets.Secrets,
                    ArrScope,
                    "user", CancellationToken.None,
                new FileDataStore("My.cal")).Result;
                // put the Information generated for the credentials object into the OAuth2Parameters-Object to access the Spreadsheets
                this.param.ClientId = GCSecrets.Secrets.ClientId; //CLIENT_ID;
                this.param.ClientSecret = GCSecrets.Secrets.ClientSecret; //CLIENT_SECRET;
                this.param.RedirectUri = "urn:ietf:wg:oauth:2.0:oob"; //REDIRECT_URI;
                this.param.Scope = ArrScope.ToString();
                this.param.AccessToken = credential.Token.AccessToken;
                this.param.RefreshToken = credential.Token.RefreshToken;
            }

            Debug.WriteLine("AuthGoogleDataInterface: Success");
            b_success = true;
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.ToString());
            b_success = false;
        }
        return b_success;
    }
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
mhaenssgen
  • 31
  • 3