googleapis / google-auth-library-java

Open source Auth client library for Java
https://developers.google.com/identity
BSD 3-Clause "New" or "Revised" License
410 stars 227 forks source link

ImpersonatedCredentials can't negotiate proper access token with delegated access to user account #759

Open tzhou2021 opened 3 years ago

tzhou2021 commented 3 years ago

Hello,

For my use case, I have to impersonate a service account B (own by our customer) that has domain wide delegation enabled using another source service account A (own by us). The goal is to access all user emails that service account B has access to using service account A.

        String emailAddress = "customer_email@gmail.com"
        ServiceAccountCredentials sourceCredentials = (ServiceAccountCredentials) ServiceAccountCredentials.fromStream(new FileInputStream("service_account_A.json")).createScoped(Arrays.asList("https://www.googleapis.com/auth/iam"));
        GoogleCredentials impersonatedCredentials = ImpersonatedCredentials.create(
                sourceCredentials,
                "service_account_B@projectB.iam.gserviceaccount.com",
                null,
                Arrays.asList("https://mail.google.com/", "https://www.googleapis.com/auth/calendar"),
                300)
               .createDelegated(emailAddress);

        try {
            impersonatedCredentials.refreshAccessToken();
        } catch (IOException e) {
            System.out.println(e);
            return;
        }
        HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(impersonatedCredentials);
        Gmail service = new Gmail.Builder(httpTransport, jacksonFactory, requestInitializer).setApplicationName("Foundation POC").build();
    ListMessagesResponse response = service.users().messages().list(emailAddress).execute();
    for (Message message : response.getMessages()) {
        System.out.println(message.toPrettyString());
    }

However, the access token retrieved doesn't have access to fetching user email, I got the error below:

Exception in thread "main" com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request
{
  "code": 400,
  "errors": [
    {
      "domain": "global",
      "message": "Precondition check failed.",
      "reason": "failedPrecondition"
    }
  ],
  "message": "Precondition check failed.",
  "status": "FAILED_PRECONDITION"
}

By further examining the implementation of createDelegated(String user) of ImpersonatedCredentials, it actually does not respect the input user, which is not what I was expecting:

Screen Shot 2021-10-08 at 3 15 44 AM

I'm using

        <dependency>
            <groupId>com.google.auth</groupId>
            <artifactId>google-auth-library-oauth2-http</artifactId>
            <version>1.2.0</version>
        </dependency>

I don't really know what could solve this issue for my unique use case, thoughts on this? Thank you!

lesv commented 3 years ago

@silvolu Could you ask someone to look at this?

TimurSadykov commented 2 years ago

Hi @tzhou2021,

The code that you referenced is expected. It is a default functionality, which is - not implemented. Individual credentials should have an override with a specific implementation. As an example you can see ServiceAccountCredential has this method implemented.

The README for this library has an example how to instantiate ImpersonatedCredentials, have you tried that?

TimurSadykov commented 2 years ago

requilifying as question

alamothe commented 2 years ago

Hello, I have the same problem. Does Google Auth not support impersonation + domain-wide delegation? If not, it's certainly a design flaw.

@TimurSadykov the answer is not satisfying as it's saying to either look at domain-wide delegation or impersonation separately, but there is no example on how to do them together. It also fails for me with "bad request" so it is a bug or just not supported at all.

alamothe commented 2 years ago

@tzhou2021 did you find a solution?

TimurSadykov commented 2 years ago

@alamothe Please provide details of your particular issue and we try to figure it out.

In the original issue the problem that @tzhou2021 misunderstood that user override is not respected by referencing GoogleCredentials. The GoogleCredentials is a base class, but actual impersonation is credential specific. If ServiceAccount is used, it has an override implementation:

@Override public GoogleCredentials createDelegated(String user) { return this.toBuilder().setServiceAccountUser(user).build(); }

alamothe commented 2 years ago

@TimurSadykov You're mixing the part where @tzhou2021 tried to debug why it doesn't work and the problem description.

Let me try to explain just the problem description.

Now, we want A to be able to perform an action on W as user U in that workspace. This should work, because permissions have been adequately set up. However it fails at runtime:

        val credentials = (GoogleCredentials.getApplicationDefault() as ServiceAccountCredentials)
                .createScoped("https://www.googleapis.com/auth/iam")
                .let {
                    ImpersonatedCredentials.create(
                            it,
                            null,
                            listOf("service-account-b@project.iam.gserviceaccount.com"),
                            listOf(DirectoryScopes.ADMIN_DIRECTORY_USER),
                            300)
                }
                .createDelegated("user-u@workspace-w.com")

        val service = Directory.Builder(
                httpTransport,
                GsonFactory.getDefaultInstance(),
                HttpCredentialsAdapter(credentials),
        )
                .setApplicationName("test")
                .build()

        service.users()
                .list()
                .setCustomer("my_customer")
                .setMaxResults(10)
                .setOrderBy("email")
                .execute()
TimurSadykov commented 2 years ago

@alamothe The use case is clear from the original description, but thanks for providing specific code you use.

I'm trying to understand the issue you are seeing. "It fails" does not provide much data to investigate.

Do you see exactly the same error like @tzhou2021? If you just do refresh token for the created credential, does it work or it throws same error?

weiminyu commented 2 years ago

@TimurSadykov

We are also seeing the same problem in our Appengine Java app. The details are below:

        ImpersonatedCredentials.newBuilder()
            .setSourceCredentials(GoogleCredentials.getApplicationDefault())
            .setTargetPrincipal(service_account_S_email)
            .setScopes(requiredScopesList)
            .build();

The error message we get back is

{
  "code": 403,
  "errors": [
    {
      "domain": "global",
      "message": "Not Authorized to access this resource/api",
      "reason": "forbidden"
    }
  ],
  "message": "Not Authorized to access this resource/api"
}

One thing we find it odd is that since ImpersonatedCredentials does not override parent's createDelegated() method, we don't have a way to set the subject's email address (user-U@our-domain.co).

By the way, the python library's service account implementation is reportedly working for domain-wide delegation. The GCP professional services even has an example here.

The python service_account code is closer to the Java JwtCredentials than to the ImpersonatedCredentials. Unfortunately, the JwTCredentials does not take another Credentials as signer. Is ImpersonatedCredentials meant to support domain-wide delegation, or should we make a feature request for JwTCredentials to accept another credential as signer?

yonghaoy commented 5 months ago

Following... Our org will disallow using SA Key soon. It would be great if our APP's SA can just impersonate domain-wide delegation SA. It might be a more common case now as using SA key is discouraged as it does introduce security risks?

It's been 2+ years since this issue opened, is there any workaround? Thanks