1

I'm trying to write console app code to update Google Directory with values pulled from a SQL database. I can't seem to make the API connect successfully when using a service account. Any ideas? I've whittled the code down to just the essentials here.


    static void Main(string[] args)
            {
                try
                { 
                    // Create a ServiceAccountCredential credential
                    var xCred = new ServiceAccountCredential(new ServiceAccountCredential.Initializer("saxxxxxxxx@directorysync-xxxxxx.iam.gserviceaccount.com")
                    {
                        Scopes = new[] {
                                DirectoryService.Scope.AdminDirectoryUser,
                                DirectoryService.Scope.AdminDirectoryUserReadonly
                            }
                    }.FromPrivateKey("-----BEGIN PRIVATE KEY-----\nMI...p9XnI4DZFO/QQJc=\n-----END PRIVATE KEY-----\n"));
    
                    // Create the service
                    DirectoryService service = new DirectoryService(
                        new BaseClientService.Initializer()
                        {
                            HttpClientInitializer = xCred,
                        }
                    );
                    
                    var listReq = service.Users.List();
                    listReq.Domain = "mycompany.com";
                    listReq.MaxResults = 100;
                    listReq.OrderBy = UsersResource.ListRequest.OrderByEnum.Email;
                    Users results = listReq.Execute();
                    //  process the users list here...
                    
                }
                catch (Exception e)
                  { Console.WriteLine(e.Message); }
            }

The error happens at the .Execute() line:

Google.Apis.Requests.RequestError
Not Authorized to access this resource/api [403]
Errors [
        Message[Not Authorized to access this resource/api] Location[ - ] Reason[forbidden] Domain[global]
]

I've tried code seen elsewhere (How to login to Google API with Service Account in C# - Invalid Credentials) to bring in the whole contents of the .JSON file that contains the credentials for the service account; that made no difference. I'm not the google domain admin, but the admin built the credential and promises that it does, indeed, have rights to the user resources. I'm utterly lost at what's not right.

Goldilox
  • 15
  • 2

1 Answers1

1

Either:

  • You're missing the email address of an admin user to impersonate.
  • An Admin of the domain needs to assign user management privileges to the service account.

Background

There's two HTTP requests involved in making a Google API request with a service account:

  1. Using the service account's credentials to request an access token from the Google OAuth 2.0 server. A HTTP POST is sent with a JWT signed with the private key of the service account. If successful, an access token is returned which is valid for one hour.

  2. Making the service API request (Admin SDK Directory API in this case) using the OAuth access token obtained from the last step.

The error message you provided is not listed in the JWT error codes page so step 1 is working, and the error is coming from step 2 - the request to Directory API.

You should be able to confirm this using an HTTPS request interceptor like mitmproxy.

You'd get a 403 error for the Directory API users.list method for a few reasons:

  1. You've authenticated as a service account, but that service account has no admin privileges which are sufficient for the request.

  2. You've authenticated as a user in the domain (either using a service account with impersonation with the sub parameter in the JWT request, or using three-legged interactive OAuth), but that user has no admin privileges which are sufficient for the request.

  3. You've authenticated as a service account or user with sufficient privileges, but the domain parameter is not owned by the customer for which the service account or user is associated with.

In your sample code, there is no email address of a domain user (admin) specified (the email address for ServiceAccountCredential.Initializer is actually the OAuth client name of the service account, not a real email address), so it is either case 1 or 3.

To address case 1

A service account has no association with a domain or access privileges by default.

An admin can open Admin console, go to "Account > Admin roles > "User Management Admin" > Admins > Assign service accounts", then:

  • Add the service account name (looks like an email address, ending @gserviceaccount.com).
  • Click "Assign role".

Now, the service account does not need to impersonate another admin in the domain, it will directly have admin privileges.

A couple of side-effects of this:

  • The service account cannot be "suspended" as such, only removed from the admin role.
  • Audit logging of the actions will show the service account name in the "Reporting > Audit > Admin" section of Admin console.

To address case 2

You may have meant to impersonate an admin user in the domain, which you could do by adding:

, User = "admin@example.com"

- after the Scopes array passed to ServiceAccountCredential.Initializer.

This adds the email address of a user to the JWT request to retrieve an access token for that user (by adding the sub field to the claim-set of the JWT assertion).

To address case 3

Replace the "mycompany.com" on the line:

listReq.Domain = "mycompany.com";

- with a domain that is associated with the customer, or instead, remove that line and add:

listReq.Customer = "my_customer";

(literally my_customer - see users.list query-parameters) - Which will list users on all domains associated with the customer (Google Workspace and Cloud Identity customers can have many secondary domains).

matja
  • 4,014
  • 3
  • 23
  • 29
  • Thanks, this looks promising. I think it is case 1; I've tested case 3 to no change, so I've now got to wait for my Google admin to return to the office to test #1 and 2; he spent the night fixing a server crash on the windows side; hopefully I can get him involved tomorrow. – Goldilox Sep 27 '21 at 19:22
  • Bear in mind that if your application should only retrieve a list of users, or individual user details, it would be best from a security point of view to use only the `AdminDirectoryUserReadonly` scope, and ask the admin to create a custom admin role with only the "Users > Read" privilege (https://support.google.com/a/answer/1219251?hl=en&ref_topic=9832445#zippy=%2Cusers), and then assign the service account to that custom role. – matja Sep 28 '21 at 10:54
  • The code is meant to update, so I do need update access. However, I got my google admin and we solved it with option 1, so THANK YOU!! – Goldilox Sep 29 '21 at 15:01