Search code examples
google-apigoogle-oauthgoogle-admin-sdkgoogle-api-dotnet-clientgoogle-workspace

Login Required 401 using Google ServiceAccountCredential using Google Admin Directory API


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


Solution

  • 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