Search code examples
javagoogle-cloud-rungoogle-secret-manager

Get ServiceAccountCredentials from ComputeEngineCredentials to do user impersonation


I'm deploying a Java API that does user impersonation via Domain-widge Delegation to access the calendar of a user. For this I have created a service account, done the delegation and given it the right permissions and access to the users calendar.

While developing locally I've been using a downloaded key for the service account in the JSON format, and pointed to it with the GOOGLE_APPLICATION_CREDENTIALS environment variable. In my code, I create the credentials like this using the Java client library:

@Bean
@Qualifier("userCredentials")
public GoogleCredentials impersonateCalendarOwner() throws IOException {
    final List<String> scopes = Collections.singletonList(CalendarScopes.CALENDAR);

    return  ((ServiceAccountCredentials) GoogleCredentials.getApplicationDefault())
            .toBuilder()
            .setServiceAccountUser(GOOGLE_CALENDARS_OWNER)
            .build()
            .createScoped(scopes);
}

This works fine locally, but when running in Cloud Run I get:

nested exception is java.lang.ClassCastException: 
class com.google.auth.oauth2.ComputeEngineCredentials cannot be cast to 
class com.google.auth.oauth2.ServiceAccountCredentials

After many hours of debugging I think I finally understand how getApplicationDefault works. I think what happens locally is this:

  1. getApplicationDefault looks first at the environment variable GOOGLE_APPLICATION_CREDENTIALS, and because it is set it reads the credentials from that file
  2. Because the file is a service account key file it creates a ServiceAccountCredentials instance so the "cast" succeeds.

And in Cloud Run it happens like this:

  1. getApplicationDefault looks first at the environment variable GOOGLE_APPLICATION_CREDENTIALS, but on Cloud Run it isn't set
  2. So it falls back to the service account identity the revision is running as by fetching the credentials from the Metadata server and returns them as an instance of ComputeEngineCredentials (since the Metadata server doesn't hand out the keys of the service account).
  3. It then tries to cast ComputeEngineCredentials to ServiceAccountCredentials which obviously doesn't work.

So now my questions remain:

  1. How can I convert some ComputeEngineCredentials to ServiceAccountCredentials?
  2. Is there some other way to get the credentials as an instance of ServiceAccountCredentials instead?
  3. Is there some other way to do user impersonation that doesn't require an instance of ServiceAccountCredentials?

My current idea is to use the ComputeEngineCredentials I get, to fetch the service account JSON key file from Secret Manager at service start up and then pass that file to ServiceAccountCredentials.fromStream but it feels like an extra step since the ComputeEngineCredentials I would be using are already the credentials of the same service account I would be fetching the key for.


Solution

  • The credentials provided by the instance metadata (Metadata server) does not include the service account private key which is required to sign requests.

    Therefore the service account impersonation code that you are trying to use will not work. You will need to use an actual service account JSON that contains the private key.

    My recommendation is to setup the default service account with a role to access Secret Manager. Store a service account JSON key file as data in Secret Manager. On your program startup fetch the service account JSON content and proceed with your impersonation code.