Gmail Java APIs to use service account with OAuth 2.0

Viewed 671

I am new to the Google API. I have a google workspace account registered with its own domain. I would like to be able to perform actions on users mailboxes using the google java api. I was using the quick start project and played with authentications as scopes.

I can perform actions on the user account that I performed my OAuth consent.

looking at the example

 String user = "me";
            ListLabelsResponse listResponse = service.users()
           .labels().list(user).execute();

It uses the "me" keyword as the user, I would like to use one of the users in the account and perform the same action. When I switch the "me" to one of the users in my domain I get the following.

GET https://gmail.googleapis.com/gmail/v1/users/user@somedomain.what.ever/labels
{
  "code" : 403,
  "errors" : [ {
    "domain" : "global",
    "message" : "Delegation denied for admin@mirncast.uk",
    "reason" : "forbidden"
  } ],
  "message" : "Delegation denied for admin@mirncast.uk",
  "status" : "PERMISSION_DENIED"
}

As far as I understand from the above I need to provide user delegation. User delegation requires a service account. So I tried to follow this tutorial

My code ended up as

Set<String> scope = Collections.singleton(GmailScopes.MAIL_GOOGLE_COM);

GoogleCredential credentialFromJson = GoogleCredential
        .fromStream(new FileInputStream(
                "/certificate.json"))
       .createScoped(scope);

GoogleCredential credential = new GoogleCredential.Builder()
        .setTransport(httpTransport)
        .setJsonFactory(jsonFactory)
        .setServiceAccountId(credentialFromJson.getServiceAccountId())
        .setServiceAccountPrivateKey(credentialFromJson.getServiceAccountPrivateKey())
        .setServiceAccountScopes(Collections.singleton(GmailScopes.MAIL_GOOGLE_COM))
        .setServiceAccountUser("user@somedomain.what.ever")
        //.setServiceAccountUser("service@custom-octagon-1234567.iam.gserviceaccount.com")
        .build();


Gmail service = new Gmail.Builder(httpTransport, jsonFactory, credential).setApplicationName("remediation service").build();

String user = "user@somedomain.what.ever";
final var messagesResponse = service.users().messages().list(user).execute();
System.out.println(messagesResponse.getMessages().stream().map(message -> message.getId()).collect(Collectors.joining(",")));

This code has a few problems.

I was only able to make it work when I set the setServiceAccountUser with the same email address I am trying to access. Using the API that way defeat the purpose from my point of view. when using the service account I get the following error

Exception in thread "main" com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request
GET https://gmail.googleapis.com/gmail/v1/users/hraman@gsuite-dev.mirncast.uk/messages
{
  "code" : 400,
  "errors" : [ {
    "domain" : "global",
    "message" : "Precondition check failed.",
    "reason" : "failedPrecondition"
  } ],
  "message" : "Precondition check failed.",
  "status" : "FAILED_PRECONDITION"
}

The example uses deprecated API GoogleCredential and I cann't figure what is the latest API for this based on OAuth2 authentication

My maven dependencies

<dependencies>
    <dependency>
        <groupId>com.google.apis</groupId>
        <artifactId>google-api-services-gmail</artifactId>
        <version>v1-rev20210301-1.31.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.oauth-client</groupId>
        <artifactId>google-oauth-client-jetty</artifactId>
        <version>1.31.4</version>
    </dependency>
</dependencies>
1 Answers

You first need to understand that gmail data is private user data, only the owner of that data can access that data, or applications which have been authorized by the user to access that data. In the case of gsuite domain accounts, the domain admin can also grant permissions to others to impersonate the original user, but impersonation is still the user accessing the data. Its just acting like its the original user. (understanding this concept will be important soon)

You need to understand that a service account is just a dummy user. It has its own google drive account, and google calendar account, it can also send emails as if it had a gmail account. So like you it can only by default access its own private data.

Now using gsuite we can delegate the users permissions allowing them to impersonate another user "user@somedomain.what.ever"

So it can be set up so that "service@custom-octagon-1234567.iam.gserviceaccount.com" is allowed to impersonate "user@somedomain.what.ever" and perform actions on their behalf. Only for this to work in code you must state which user you are currently impersonating in this case we use setServiceAccountUser if you dont do that then you are just "service@custom-octagon-1234567.iam.gserviceaccount.com" and can only access the data belonging to "service@custom-octagon-1234567.iam.gserviceaccount.com"

As for accessing data with messages.list the following commands are interchangeable.

service.users().messages().list("user@somedomain.what.ever")
service.users().messages().list("me")

With the exception of if the first option is not the email address of the currently authenticated user, or the impersonating user it will fail. By impersonating a user you are in fact that user so the API sees the service account as "user@somedomain.what.ever" without specifying the impersonation you are just normal old service account who can only access their data.

There needs to be a way of specifying that all calls coming from the service account are now on behalf of this user that's why you need to set setServiceAccountUser to the user you wish to impersonate.

Related