7

I am writing a back-end process in Java that will impersonate a user and add/remove documents on their Google Drive.

The server account seems to authenticate correctly but when I try to impersonate a user, I get a 401 Unauthorized error. Please see below for details.

Configuration

I have configured the server account as follows:

  • Created a project under Google APIs and enabled Google Drive API
  • Created a service account called anothertest@yyyyyyyyy.iam.gserviceaccount.com, set the role as Service Account Actor and given it domain-wide delegation. It has Client ID 110xxxxxxxxx342
  • I have download the P12 key file

I have configured the domain using the Manage API client access screen to authorize 110xxxxxxxxx342 to have the scope: https://www.googleapis.com/auth/drive.

Google Support have looked at my configuration and have given it the thumbs up.

My code then looks as follows:

package com.dcm.sharingdocuments;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import com.google.api.client.auth.oauth2.TokenErrorResponse;
import com.google.api.client.auth.oauth2.TokenResponseException;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.FileList;

public class SharingDocumentsTest3 {

    private static final String SERVICE_ACCOUNT_EMAIL = " anothertest@yyyyyyyyy.iam.gserviceaccount.com";

    public static Drive getDriveService(String userEmail) throws Exception {

        File keyFile = new File("E:\\Projects\\Workspace\\Sharing Documents\\authentication\\AnotherTestKeyFile.p12");

        HttpTransport httpTransport = new NetHttpTransport();
        JacksonFactory jsonFactory = new JacksonFactory();

        List<String> SCOPES = Arrays.asList(DriveScopes.DRIVE_METADATA_READONLY);

        GoogleCredential credential = null;

        if (userEmail == null) {

            credential = new GoogleCredential.Builder().setTransport(httpTransport).setJsonFactory(jsonFactory)
                    .setServiceAccountId(SERVICE_ACCOUNT_EMAIL).setServiceAccountScopes(SCOPES)
                    .setServiceAccountPrivateKeyFromP12File(keyFile).build();

            credential.refreshToken();

        } else {

            credential = new GoogleCredential.Builder().setTransport(httpTransport).setJsonFactory(jsonFactory)
                    .setServiceAccountId(SERVICE_ACCOUNT_EMAIL).setServiceAccountScopes(SCOPES)
                    .setServiceAccountPrivateKeyFromP12File(keyFile).setServiceAccountUser(userEmail).build();

            credential.refreshToken();

        }

        Drive service = new Drive.Builder(httpTransport, jsonFactory, null).setHttpRequestInitializer(credential)
                .build();

        return service;

    }

    public static void main(String[] args) {

        SharingDocumentsTest3 sdt3 = new SharingDocumentsTest3();

        sdt3.execute();

    }

    private void execute() {

        try {

            Drive service = getDriveService(null);

            Drive services = getDriveService("anzzzze@zzzzz.me.uk");

            displayFiles(services);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void displayFiles(Drive service) throws Exception {

        FileList result = service.files().list().setPageSize(10).execute();
        List<com.google.api.services.drive.model.File> files = result.getFiles();

        if (files == null || files.size() == 0) {

            System.out.println("No files found.");

        } else {

            System.out.println("Files:");
            for (com.google.api.services.drive.model.File file : files) {
                Set<Entry<String, Object>> entries = file.entrySet();

                Iterator<Entry<String, Object>> it = entries.iterator();

                while (it.hasNext()) {
                    Entry<String, Object> entry = it.next();
                    String key = entry.getKey();
                    Object value = entry.getValue();

                    if (value instanceof String) {
                        System.out.println("\tKey = " + key + ", Value = " + (String) value);
                    } else {
                        System.out.println("\tKey = " + key + ", Value = " + value.toString());
                    }

                }

                System.out.printf("%s (%s)\n", file.getName(), file.getId());
            }
        }
    }
}

When I run the code as is above, I get the error:

    Mar 29, 2017 9:55:27 AM com.google.api.client.googleapis.services.AbstractGoogleClient <init>
    WARNING: Application name is not set. Call Builder#setApplicationName.
    com.google.api.client.auth.oauth2.TokenResponseException: 401 Unauthorized
        at com.google.api.client.auth.oauth2.TokenResponseException.from(TokenResponseException.java:105)
        at com.google.api.client.auth.oauth2.TokenRequest.executeUnparsed(TokenRequest.java:287)
        at com.google.api.client.auth.oauth2.TokenRequest.execute(TokenRequest.java:307)
        at com.google.api.client.googleapis.auth.oauth2.GoogleCredential.executeRefreshToken(GoogleCredential.java:384)
        at com.google.api.client.auth.oauth2.Credential.refreshToken(Credential.java:489)
        at com.dcm.sharingdocuments.SharingDocumentsTest3.getDriveService(SharingDocumentsTest3.java:50)
        at com.dcm.sharingdocuments.SharingDocumentsTest3.execute(SharingDocumentsTest3.java:75)
        at com.dcm.sharingdocuments.SharingDocumentsTest3.main(SharingDocumentsTest3.java:65)

So the code fails at credential.refreshToken() when I set the setServiceAccountUser. It appears to have successfully refreshed the token when I do not. I have tried various combinations of this code – e.g. commented out the refreshToken() lines, commented out the getDriveService(null) line – but whenever I try to use/refresh the credential obtained for the impersonated user I get the 401 Unauthorized error.

If I modify the code so that the drive obtained by getDriveService(null) is passed to DisplayFiles(...), then I get one file listed called “Getting Started”. So it seems that the service account authorization is working and Google have added their default file to the Drive for the server account.

I am using google-*1.22.0.jar files and Java 1.8 to the run the above code

The problem I think is in the way I have configured the domain or the way I am trying to impersonate the user but my code looks as many examples on the web do and Google Support appear to say that I have configured the domain correctly.

Anything you can suggest as a resolution or next step would be much appreciated!

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
Chris B.
  • 71
  • 6
  • Check the following question http://stackoverflow.com/questions/33602347/using-google-drive-api-from-a-java-application where this seem to work. – Konrad Mar 29 '17 at 09:57
  • Hi Konrad, many thanks for responding so quickly. I have looked at the question but the user's problem is resolved by removing setServiceAccountUser(...) thereby not impersonating a user. This is what I am experiencing too. The code runs provided I don't include setServiceAccountUser(...) but I need to include this method to impersonate a user and get access to their files. – Chris B. Mar 29 '17 at 10:26
  • 1. You should let the Client library deal with refreshing your access. That and service accounts don't have refresh tokens. 2. DriveScopes.DRIVE_METADATA_READONLY <-- read only access 3. service account quick start https://developers.google.com/identity/protocols/OAuth2ServiceAccount – Linda Lawton - DaImTo Mar 29 '17 at 11:36
  • Hi DalmTo, I agree about the refreshing of tokens. It is not something I expected to do but is there primarily as a way of catching the error soonest. I would not expect to do this in a production environment for what I am trying to do. I have tried all the DriveScopes - including all() and I get the same error each time. I have read the quick start page many times - and yet still my code fails! – Chris B. Mar 29 '17 at 11:45
  • 1
    Have you managed to solve this? Facing the same problem here... – Victor Gomes Aug 07 '17 at 18:45
  • Hi Victor, Unfortunately I was unable to resolve the issue. I spoke to Google support too but got nowhere. Reluctantly, I have had to move on and am now using Microsoft's OneDrive for Business. – Chris B. Aug 08 '17 at 21:23
  • I'm having this same issue and this is the only Stack Overflow question (and I have read a lot of them) that seems to accurately describe the problem I having... – Alistair Jones Feb 14 '18 at 17:58

1 Answers1

1

I have been stuck on this problem for a long time and I finally found my problem. There is definitely a bug in the "Manage API client access" Admin console...

You must put the "Client ID" (e.g. 110xxxxxxxxx342) for the client name and NOT the "Service Account ID" (the one that looks like an email). Now, their documentation is correct, and they do say in the documentation to use the Client ID, I have to give them that.

So here is the bug. When I arrived to the Manage API screen, I saw "Example: www.example.com". I typed in the Service Account ID there, thinking that the email address format matched "www.example.com" better than the Client ID. I pressed "Authorize", and the entry had clearly been accepted and everything was good. The result looks like this:

Incorrectly configured screenshot

It even generated the Client ID from the Service ID! Great! Except my code gets a 401 error every time I try to connect with setServiceUser().

If I return to the Manage API Client Access console and if I remove the previous entry and perform the same actions except use the Client ID instead of the Service ID. The result is this:

Visible identical screenshot, but correctly configured

Exactly the same, but now I don't get a 401 error. So there is NO WAY to look at the console, and know if you have it successfully configured it or not. I tested this 3 times to make sure I wasn't losing my mind...

Alistair Jones
  • 553
  • 4
  • 10
  • Hi Alistair, Excellent news! Well spotted. I will try it out for myself but I think you cracked it. – Chris B. Feb 20 '18 at 16:45
  • 1
    I'm having the same error (401 when delegating) and have followed all the same steps, including the one that solved it for you. I likely missed a step in the GSuite or GCP config but I can't for the life of me figure it out. Any chance I could bother you to post an update that included each of those GSuite and GCP steps? Thanks! :) – Patrick Lightbody Dec 11 '19 at 14:48
  • Quick update in case others, like me, are still having problems: I found that I needed to fiddle with the authorized API scores. My test API call was to list the CalendarList entries and I had granted https://www.googleapis.com/auth/calendar, which should be more than enough permissions. But I received a 401 until I "expanded" the scope to be "https://www.googleapis.com/auth/calendar,https://www.googleapis.com/auth/calendar.readonly", which frankly makes no sense to me but it worked! – Patrick Lightbody Dec 11 '19 at 20:03