4
votes

I'm looking for a Google Drive API authentication scheme that will give a service application (on a server) rights to create shared documents in a Drive folder, without user interaction.

Google's current unique name for the specific authentication scheme I should use for this is probably a sufficient answer to this question.

Although the document creation will occur in response to a user action, the documents will not be permanently associated with those users and I do not wish to require any user to present a Google account. Instead, I wish the user to be able to access the document via an "Anyone with the link can edit"-type URL displayed on a web page after the document is created.

This is intended to automatically generate documents for multiple generally anonymous people to collaborate, and all documents will be stored in a single folder.

There's a good chance this is a duplicate of this question: Google Drive API username + password authentication. Unfortunately, the accepted answer doesn't contain enough information for me to find my way now that the links it references are dead.

It may also be a duplicate of other questions that have accepted but unclear answers, such as: .NET Google Docs API Authentication (without user interaction), How do I authenticate Google Calendar API v3 without user interaction?, and Drive API doc upload from a server without user interaction.

1
I may have found the solution in indirectly related answers here stackoverflow.com/questions/28825860/… and here stackoverflow.com/questions/21708305/…. It's possible that a service account is what I need, and the reason it is asking for human authentication is that my token type is incorrect (should be PKCS, was JSON).shannon

1 Answers

2
votes

Authenticating as a service account was the approach I needed.

The Google SDK actions were simply misleading. When I provided some incorrect values it fell back to user-based authentication (automatically opening a web browser to request interactive credentials). I incorrectly interpreted this to mean that the service account functionality was implemented as a long-term key approved by and in the context of a specific interactive user, or something similar.

No user interaction was necessary, however the .p12 certificate was required, rather than whatever credentials the default .json file provided (which I had tried using in a number of ways). Here's the code I used:

using Google.Apis.Auth.OAuth2;
using Google.Apis.Drive.v2;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Http;
using Google.Apis.Services;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;

using GData = Google.Apis.Drive.v2.Data;

public class Drive
{
    private const string GoogleDocMimeType = "application/vnd.google-apps.document";

    /// <summary>
    /// Creates a drive service, authenticated using information found in the Google Developers Console under "APIs & auth / Credentials / OAuth / Service account"
    /// </summary>
    /// <param name="svcEmail">The service account "Email address"</param>
    /// <param name="certPath">The path to the service account "P12 key" file</param>
    public Drive(string svcEmail, string certPath)
    {
        Service = AuthenticateService(svcEmail, certPath);
    }

    private DriveService Service
    {
        get;
        set;
    }

    /// <summary>
    /// Creates a "Google Doc" and shares it with anyone with the link
    /// </summary>
    /// <param name="title"></param>
    /// <returns>The drive FileId, accessible at https://docs.google.com/document/d/FileId </returns>
    public async Task<string> CreateShared(string title)
    {
        var fileId = await CreateDocument(title);
        await ShareFile(fileId);
        return fileId;
    }

    private async Task<string> CreateDocument(string title)
    {
        var file = new GData.File
        {
            Title = title,
            MimeType = GoogleDocMimeType
        };
        file = await Service.Files.Insert(file).ExecuteAsync();
        return file.Id;
    }

    private async Task ShareFile(string fileId)
    {
        Permission permission = new Permission
        {
            Type = "anyone",
            Role = "writer",
            WithLink = true
        };
        var a = Service.Permissions.Insert(permission, fileId);
        await a.ExecuteAsync();
    }

    private static DriveService AuthenticateService(string svcEmail, string certPath)
    {
        string[] scopes = new[] { DriveService.Scope.DriveFile };
        X509Certificate2 certificate = new X509Certificate2(certPath, "notasecret", X509KeyStorageFlags.Exportable);

        var init = new ServiceAccountCredential.Initializer(svcEmail) { Scopes = scopes };
        IConfigurableHttpClientInitializer credential = new ServiceAccountCredential(init.FromCertificate(certificate));

        return new DriveService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = "Document Management Service",
        });
    }
}

And here's an experimental consumer:

internal class Program
{
    private const string svcEmail = "[email protected]";
    private const string certPath = @"credentials\projectname-fingerprintprefix.p12";
    private readonly static Drive drive = new Drive(svcEmail, certPath);

    private static void Main(string[] args)
    {
        string id = drive.CreateShared("my title").Result;
        Console.WriteLine(id);
    }
}

This seems to use Google Drive storage in an isolated, application/project-specific data repository. According to other posts, there is no way to get an interactive Drive UI view on that. I don't know if if it uses my personal storage quota, etc. But, this is the best approach I have so-far and I'll answer those questions for myself (not here) next.