Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

401 Unauthorized error when using a server account to impersonate a user in order to access their Google Drive

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 [email protected], 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 = " [email protected]";

    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("[email protected]");

            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!

like image 340
Chris B. Avatar asked Nov 19 '22 00:11

Chris B.


1 Answers

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...

like image 84
Alistair Jones Avatar answered Dec 22 '22 00:12

Alistair Jones