36

I'm beating myself bloody trying to get a simple service acccount login to work in C#, to Google API and Google Analytics. My company is already getting data into Analytics, and I can query information with their Query Explorer, but getting started in .Net is not going anywhere. I am using a Google-generated json file with PKI, as the documentation says that such a service account is the proper way for computer-to-computer communication with Googla API. Code snipet:

public static GoogleCredential _cred;
public static string _exePath;

static void Main(string[] args) {
    _exePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Replace(@"file:\", "");
    var t = Task.Run(() => Run());
    t.Wait();
}

private static async Task Run() {
    try {
        // Get active credential
        using (var stream = new FileStream(_exePath + "\\Default-GASvcAcct-508d097b0bff.json", FileMode.Open, FileAccess.Read)) {
            _cred = GoogleCredential.FromStream(stream);
        }
        if (_cred.IsCreateScopedRequired) {
        _cred.CreateScoped(new string[] { AnalyticsService.Scope.Analytics });
        }
        // Create the service
        AnalyticsService service = new AnalyticsService(
            new BaseClientService.Initializer() {
                HttpClientInitializer = _cred,
            });
        var act1 = service.Management.Accounts.List().Execute(); // blows-up here

It all compiles fine, but when it hit the Execute() statement, a GoogleApiException error is thrown:

[Invalid Credentials] Location[Authorization - header] Reason[authError] Domain[global]

What am I missing?

Vic Lindsey
  • 789
  • 1
  • 6
  • 9

6 Answers6

42

It appears that the GoogleAnalytics cannot consume a generic GoogleCredential and interpret it as a ServiceAccountCredential (even though it is acknowledged, interally, that it is actually of that type). Thus you have to create a ServiceAccountCredential the hard way. It’s also unfortunate that GoogleCredential does not expose the various properties of the credential, so I had to build my own.

I used the JSON C# Class Generator at http://jsonclassgenerator.codeplex.com/ to build a "personal" ServiceAccountCredential object using the JSON library that is an automatic part of Google API (Newtonsoft.Json), retrieved essential parts of the downloaded json file of the service account, to construct the required credential, using its email and private key properties. Passing a genuine ServiceAccountCredential to the GoogleAnalytics service constructor, results in a successful login, and access to that account’s allowed resources.

Sample of working code below:

using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.Analytics.v3;
using Newtonsoft.Json;
    .
    .
    .
try
{
    // Get active credential
    string credPath = _exePath + @"\Private-67917519b23f.json";

    var json = File.ReadAllText(credPath);
    var cr = JsonConvert.DeserializeObject<PersonalServiceAccountCred>(json); // "personal" service account credential

    // Create an explicit ServiceAccountCredential credential
    var xCred = new ServiceAccountCredential(new ServiceAccountCredential.Initializer(cr.ClientEmail)
    {
        Scopes = new[] {
            AnalyticsService.Scope.AnalyticsManageUsersReadonly,
            AnalyticsService.Scope.AnalyticsReadonly
        }
    }.FromPrivateKey(cr.PrivateKey));

    // Create the service
    AnalyticsService service = new AnalyticsService(
        new BaseClientService.Initializer()
        {
            HttpClientInitializer = xCred,
        }
    );

    // some calls to Google API
    var act1 = service.Management.Accounts.List().Execute();

    var actSum = service.Management.AccountSummaries.List().Execute();

    var resp1 = service.Management.Profiles.List(actSum.Items[0].Id, actSum.Items[0].WebProperties[0].Id).Execute();

Some may wonder what a Google-generated service account credential with PKI (Private Key) looks like. From the Google APIs Manager (IAM & Admin) at https://console.developers.google.com/iam-admin/projects, select the appropriate project (you have at least one of these). Now select Service accounts (from the left nav links), and CREATE SERVICE ACCOUNT at top of screen. Fill in a name, set the Furnish a new private key checkbox, then click Create. Google will cause an automatic download of a JSON file, that looks something like this:

{
  "type": "service_account",
  "project_id": "atomic-acrobat-135",
  "private_key_id": "508d097b0bff9e90b8d545f984888b0ef31",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...o/0=\n-----END PRIVATE KEY-----\n",
  "client_email": "google-analytics@atomic-acrobat-135.iam.gserviceaccount.com",
  "client_id": "1123573016559832",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-analytics%40atomic-acrobat-135923.iam.gserviceaccount.com"
}
Vic Lindsey
  • 789
  • 1
  • 6
  • 9
  • Looks like someone used your code and used it on his blog. coz you answered this back in 2016 and his blog is just recent. My question is can I get an access token using this..so I can use the access token for the embed api. coz i'm getting a null access token. – CyberNinja Jun 27 '17 at 17:18
  • 3
    I was too lazy to use your class generator stuff. So instead I did this: `Newtonsoft.Json.Linq.JObject cr = (Newtonsoft.Json.Linq.JObject) JsonConvert.DeserializeObject(json); string s = (string) cr.GetValue("private_key");` Thank you for your answer-- it helped a lot. – robertpb Jul 20 '17 at 13:32
  • 1
    Thanks to the answer from dlumpp, I have been able to confirm that using the GoogleCredential.FromStream (or FromFile) works fine as long as you set the scope correctly. https://stackoverflow.com/users/6753705/dlumpp – Mark Adamson Aug 12 '19 at 20:51
  • Doing this gives me a "`MailKit.Security.AuthenticationException: '334: eyJzdGF0dXMiOiI0M...`", however if I use a certificate it works! Great... – Murphybro2 Oct 23 '19 at 09:28
  • I've spent some time playing around with this stuff and suddenly my key works. Out of interest I changed some of the characters to see what would happen, and depending on which I change, it still works o_O – Murphybro2 Oct 23 '19 at 14:48
  • any idea how i can use same with bigqueryclient? – Jay Magwadiya Nov 20 '19 at 12:35
8

The invalid credentials error is happening because the scopes you specified aren't actually getting sent with your credentials. I made the same mistake and only realized after I debugged and still saw 0 scopes on the credential after the CreateScoped call.

A GoogleCredential is immutable so CreateScoped creates a new instance with the specified scopes set.

Reassign your credentials variable with the scoped result like so and it should work:

  if (_cred.IsCreateScopedRequired) {
    _cred = _cred.CreateScoped(AnalyticsService.Scope.Analytics);
  }

The accepted answer works because it's achieving the same thing in a more difficult way.

dlumpp
  • 891
  • 8
  • 14
7

In 2020 you don't need to do all this and the GoogleCredential works fine. The code in the question looks correct except for one line:

credentials.CreateScoped(new string[] { DriveService.Scope.Drive });

The CreateScoped method returns a copy of the credentials. If you reassign it back to itself it just works.

For the sake of completeness, this is my test code that works perfectly:

using (var stream =
            new FileStream("drive-credentials.json", FileMode.Open, FileAccess.Read))
        {                
            var credentials = GoogleCredential.FromStream(stream);
            if (credentials.IsCreateScopedRequired)
            {
                credentials = credentials.CreateScoped(new string[] { DriveService.Scope.Drive });
            }


            var service = new DriveService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credentials,
                ApplicationName = "application name",                    
            });

            FilesResource.ListRequest listRequest = service.Files.List();
            listRequest.PageSize = 10;
            listRequest.Fields = "nextPageToken, files(id, name)";

            // List files.
            IList<Google.Apis.Drive.v3.Data.File> files = listRequest.Execute()
                .Files;
        }
Mario Lopez
  • 1,405
  • 13
  • 24
  • This answer should be the one used in the Google Documentation because theirs still loads the pl12 cert. It took me a while to find this even after writing my own parsing classes.. Now it works fine. Thanks – Piotr Kula Nov 08 '22 at 22:09
7

FOR 2020, the call is made as follows:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Google.Apis.Services;
using Google.Apis.Auth.OAuth2;
using System.IO;
using Google.Apis.Sheets.v4;
using Google.Apis.Sheets.v4.Data;

namespace SistemasInfinitos.Controllers.Google.Apis.Sample.MVC4
{
    public class SpreadsheetseController : Controller
    { 
        public ActionResult IndexAPI()
        {
            //accede a las credenciales
            var stream = new FileStream(Server.MapPath("~/quickstart2-9aaf.json"),
                FileMode.Open
               // FileAccess.Read//SOLO LECTURA
                );
            //abre las credenciales
            var credentials = GoogleCredential.FromStream(stream);

            //virifica las credenciales
            if (credentials.IsCreateScopedRequired)
            {
                credentials = credentials.CreateScoped(new string[] { SheetsService.Scope.Spreadsheets });
            }
            ///inicializa la api
        var service = new SheetsService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credentials,
                ApplicationName = "SistemasInfinitos",
            });

            // Define los parametros.  
            String spreadsheetId = "1MKxeqXV5UEMXU2yBe_xi0nwjooLhNN6Vk";
            String range = "Sheet1";
            SpreadsheetsResource.ValuesResource.GetRequest request =service.Spreadsheets.Values.Get(spreadsheetId, range);
            // imprime   
            ValueRange response = request.Execute();
            IList<IList<Object>> values = response.Values;
            ViewBag.List = values;
            return View();
        }
    }
}

and View

@{
    ViewBag.Title = "IndexAPI";
}

<div class="col-md-6">
    <h3>Read Data From Google Live sheet</h3>
    <table class="table" id="customers">
        <thead>
            <tr>
                <th>
                    id
                </th>
                <th>
                    Name
                </th>
            </tr>
        </thead>
        <tbody>
            @{
                foreach (var item in ViewBag.List)
                {
                    <tr>
                        <td>@item[0]</td>
                        <td>@item[1]</td>
                    </tr>

                }
            }
        </tbody>

    </table>
</div>
1

Another option is to use GoogleCredential.GetApplicationDefault(). I believe this is the currently (Oct. 2018) recommended approach. Here's some F#, but it's more or less the same in C# modulo syntax:

let projectId = "<your Google Cloud project ID...>"
let creds =
  GoogleCredential.GetApplicationDefault()
    .CreateScoped(["https://www.googleapis.com/auth/cloud-platform"])
use service =
  new CloudBuildService(
    BaseClientService.Initializer(HttpClientInitializer=creds))
let foo = service.Projects.Builds.List(projectId).Execute()

Now, just make sure that you set the GOOGLE_APPLICATION_CREDENTIALS to point to the file with the credentials JSON file, eg. GOOGLE_APPLICATION_CREDENTIALS=creds.json dotnet run.

skainswo
  • 356
  • 2
  • 5
0

If you arrive here while trying to determine how to create a ServiceAccountCredential, without using a key file directly you might be interested to know that the method below will work (sometimes):

GoogleCredential credential = GoogleCredential.GetApplicationDefault();

ServiceAccountCredential serviceAccountCredential = 
    credential.UnderlyingCredential as ServiceAccountCredential;
derekbaker783
  • 8,109
  • 4
  • 36
  • 50