3

I want to invoke a Cloud Run from an external application. The external application is written in Kotlin (java) and run on the JDK 11 JVM. It authenticates using a service account

ServiceAccountCredentials.fromStream(serviceAccount).createScoped("https://www.googleapis.com/auth/cloud-platform")

where the service account is is a valid JSON file. I have tested the permissions to invoke a cloud run for the service account inside the google cloud console.

I have not found an official API so I used google's GoogleNetHttpTransport to run an HTTP post request. To invoke the cloud run I have tried to use the HttpCredentialsAdapter

transport.createRequestFactory(HttpCredentialsAdapter(googleCredentials))
   .buildPostRequest(GenericUrl(url), JsonHttpContent(JacksonFactory(), data))

I have tried to use the access token directy

val headers = HttpHeaders()
googleCredentials.refreshIfExpired()
headers.authorization = "Bearer " + googleCredentials.accessToken.tokenValue
postRequest.headers = headers

And I have tried to use a jet service account and use the jwt request metadata

val headers = HttpHeaders()
jwtCredentials = ServiceAccountJwtAccessCredentials.fromStream(serviceAccount)
jwtCredentials.getRequestMetadata(URI(url)).forEach {
    headers.set(it.key, it.value)
}
postRequest.headers = headers

In all these cases it returns this error

[20:35:00 WARN]: Exception in thread "TestThread" com.google.api.client.http.HttpResponseException: 401 Unauthorized
[20:35:00 WARN]:    at com.google.api.client.http.HttpRequest.execute(HttpRequest.java:1113)
[20:35:00 WARN]:    at ***.CloudRun$invoke$3.invokeSuspend(CloudRun.kt:34)
[20:35:00 WARN]:    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
[20:35:00 WARN]:    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
[20:35:00 WARN]:    at java.base/java.lang.Thread.run(Thread.java:834)

Thanks for your support in advance

Gabber235
  • 168
  • 2
  • 8
  • What do you mean by "Invoke a Cloud Run"? Cloud Run is a platform/environment for hosting REST / HTTP applications. You create an application that listens on $PORT and responds to HTTP requests. Do you mean invoke a custom REST application that is hosted in a container? If yes, what is the nature of that application? – Kolban Apr 02 '20 at 19:08
  • I want to post a request to another REST application that is running inside the cloud run container. The cloud run is used for various tasks and is written in node js. – Gabber235 Apr 02 '20 at 19:15

2 Answers2

3

I spent time on the OAuth2 Java library and the problem is the following. The class ServiceAccountCredentials add an AccessToken as authorization, and the class ServiceAccountJwtAccessCredentials add a self-signed identity token.

I will go deeper into it, but for now you can set the header manually

        String myUri = "https://.....";

        Credentials credentials =  ServiceAccountCredentials
                .fromStream(resourceLoader.getResource("classpath:./key.json")
                        .getInputStream()).createScoped("https://www.googleapis.com/auth/cloud-platform");

        String token = ((IdTokenProvider)credentials).idTokenWithAudience(myUri, Collections.EMPTY_LIST).getTokenValue();

        HttpRequestFactory factory = new NetHttpTransport().createRequestFactory();
        HttpRequest request = factory.buildGetRequest(new GenericUrl(myUri));
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization","Bearer " + token);
        request.setHeaders(headers);
        HttpResponse httpResponse = request.execute();
        System.out.println(CharStreams.toString(new InputStreamReader(httpResponse.getContent(), Charsets.UTF_8)));

The pieces are here, but not work together.

EDIT

After some exchange on GitHub, a Google provide me the solution. Now the header is automatically provisioned with a signed id Token

        Credentials credentials =  ServiceAccountCredentials
                .fromStream(resourceLoader.getResource("classpath:./key.json")
                        .getInputStream()).createScoped("https://www.googleapis.com/auth/cloud-platform");


        IdTokenCredentials idTokenCredentials = IdTokenCredentials.newBuilder()
                .setIdTokenProvider((ServiceAccountCredentials)credentials)
                .setTargetAudience(myUri).build();

        HttpRequestFactory factory = new NetHttpTransport().createRequestFactory(new HttpCredentialsAdapter(idTokenCredentials));
        HttpRequest request = factory.buildGetRequest(new GenericUrl(myUri));
        HttpResponse httpResponse = request.execute();
        System.out.println(CharStreams.toString(new InputStreamReader(httpResponse.getContent(), Charsets.UTF_8)));
guillaume blaquiere
  • 66,369
  • 2
  • 47
  • 76
2

I don't know Java SDKs well enough, but I think I spotted the error:

headers.authorization = "Bearer " + googleCredentials.accessToken.tokenValue
                                                      ^^^^^^^^^^^

You should not be sending a GCP "access token" to a Cloud Run application (e.g. https://*.run.app). That token is for calling Google’s APIs; and Cloud Run apps are not that.

Instead, you should be sending an "identity token", which basically shows that you are in possession of the service account, without giving out any permission to the called service (to use that token to call GCP APIs).

You cannot sign an "identity token" yourself locally, you need to call an endpoint.

This is explained in this page: https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-jwt

ahmet alp balkan
  • 42,679
  • 38
  • 138
  • 214
  • This was what I needed specifically that set me on the right track for the general problem instead of the java specific problem. – Niko Feb 01 '22 at 19:32