1
votes

I have tried to follow the simple example listed here: https://developers.google.com/admin-sdk/directory/v1/quickstart/dotnet

The difference is I generated a Service Account Credential, and assigned it as a Delegate with the Role Project Owner, so it has full access. I also assigned it the proper namespaces for scopes.

Here it has access to orgunits which is what I'm trying to list in the Directory API

scopes are assigned

Here is my service account defined

enter image description here

Here are my credentials

enter image description here

I downloaded the JSON for the credential and added it to my project. I can confirm that the code loades the ServiceAccountCredential and successfully authenticates and gets an access token by inspecting the debugger.

But then I pass the credential to the Service Initializer, and when I create and execute a request it fails with

{"Google.Apis.Requests.RequestError\r\nLogin Required [401]\r\nErrors [\r\n\tMessage[Login Required] Location[Authorization - header] Reason[required] Domain[global]\r\n]\r\n"}

Here's the code:

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using System;
using System.Collections.Generic;
using System.IO;

namespace DirectoryQuickstart
{
    class Program
    {
        static string[] Scopes = { DirectoryService.Scope.AdminDirectoryUser, DirectoryService.Scope.AdminDirectoryOrgunit };
        static string ApplicationName = "slea-crm";
        static string Secret = "gsuite-secret.json";

        static void Main(string[] args)
        {

            ServiceAccountCredential sac = GoogleCredential.FromFile(Secret).CreateScoped(Scopes).UnderlyingCredential as ServiceAccountCredential;

            var token = sac.GetAccessTokenForRequestAsync().Result;

            // Create Directory API service.
            var service = new DirectoryService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = sac,

                ApplicationName = ApplicationName,
            });

            OrgunitsResource.ListRequest request = service.Orgunits.List(customerId: "REDACTED");
            IList<OrgUnit> orgUnits = request.Execute().OrganizationUnits;

            if (orgUnits != null && orgUnits.Count > 0)
            {
                foreach (var orgUnit in orgUnits)
                {
                    Console.WriteLine("{0} ({1})", orgUnit.Name, orgUnit.OrgUnitPath);
                }
            }
            else
            {
                Console.WriteLine("No orgunits found.");
            }
            Console.Read();

        }
    }
}

Here is the content of my JSON secret (with redactions)

enter image description here

What am I missing here?

EDIT: OK, I breakpoint the code while it generates the request, and I can see that no where does it set the Authorization token bearer in the headers. Why? I would expect this HttpClientInitializer class to take care of that, since the API docs say it knows how to handle that, and every example on the internet I've found shows it just passing the credential into the service initializer. But when I walked through it, even though the credential has already been granted an access token and one exists within it, nowhere does the request have the header updated.

The only thing I can see is there is some way to add an HTTP request interceptor where possibly I could do this myself, but wow, this seems really...bizarre -- after all this work they did on the dotnet client SDK, I honestly could have just written direct to the HTTP API and it would have been a lot simpler and easier to follow.

enter image description here

2
Yes, I did. See the answer I just posted below. Man this was a frustrating endeavor. - zenocon

2 Answers

4
votes

The missing piece of the puzzle is this line:

ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
    .CreateScoped(Scopes)
    .UnderlyingCredential as ServiceAccountCredential;

Needs to be modified to this:

static string userName = "[email protected]" // valid user in your org

ServiceAccountCredential sac = GoogleCredential.FromFile(Secret)
    .CreateScoped(Scopes)
    .CreateWithUser(userName)
    .UnderlyingCredential as ServiceAccountCredential;

Java/Python/Go sample of doing similar is here: https://developers.google.com/admin-sdk/directory/v1/guides/delegation#create_the_service_account_and_its_credentials

0
votes

This has been answered but adding more details here. If anyone wants to impersonate user to upload file on google drive using Service account. Follow these steps

  • Create Service Account
  • Enable Site Wide delegation for service account
  • Get Service account client ID
  • Enable Client ID to use Google Drive API using Google Admin Console->Manage API
  • Use the below C# code to upload file

    public static DriveService GetService()
    {
        string[] scopes = new string[] { DriveService.Scope.Drive };
    
    
       //"SERVICE_ACCOUNT_EMAIL_HERE";
        String serviceAccountEmail = "[email protected]";
    
        // Scope and user email id which you want to impersonate
        var initializer = new ServiceAccountCredential.Initializer(serviceAccountEmail)
        {
            Scopes = scopes,
            User = "[email protected]"
        };
    
        //get private key, from .JSON file
        var credential = new ServiceAccountCredential(initializer.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkHeAicu6uFQn0\n7KUVTjgZ68nQui8+c8NmKW8aW8vhkBIKfdewXFECiUlTMPyI+HXbubsCK5Dl2xBS\nnphLq6YyE0xEQxNFLYHwfUKuzGQ2rV+qObcZ0mLZjCaf+pw3YiRVuU6OtslLJKJH\n-----END PRIVATE KEY-----\n"));
    
    
        // Create the service.
        var service = new DriveService(new BaseClientService.Initializer()
        {
            HttpClientInitializer = credential,
            ApplicationName = "DriveAPI",
        });
    
    
    
    
        service.HttpClient.Timeout = TimeSpan.FromMinutes(100);
        return service;
    }
    

That's it, we are done above Code is using Impersonation/Delegation for uploading file on Google Drive using Service account

Reference : Upload file to Google Drive using Service Account in C# MVC (With Impersonation)