11

Here my use case.

  • I already have a Cloud Run service deployed in private mode. (same issue with Cloud Function)
  • I'm developing a new service that use this Cloud Run. I use the default credential in the application for the authentication. It worked on Compute Engine and on Cloud Run because the default credential is gotten from the metadata server.

But, on my local environment, I need to use a service account key file for achieving this. (for example, when I set the GOOGLE_APPLICATION_CREDENTIALS env var)

However, I can't use my user account credential.

The log trace are very clear in Java, Node or Go: impossible to generate an identity token on a user credential type.

So,

  • why google auth library doesn't allow to do this?
  • Why Google Front End accept only Google signed identity token?
  • Is a workaround exist for continuing to use the user account?

And a piece of context: we want to avoid to use service account key file. Today, it's our major security breach (files are copied, sent by email, even committed on Github publicly...). The user account default credential work with all Google Cloud API but not with IAP and CLoud Run/Functions.

EDIT

Here some example of error.

JAVA

I do this

        Credentials credentials = GoogleCredentials.getApplicationDefault().createScoped("https://www.googleapis.com/auth/cloud-platform");

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

        HttpRequestFactory factory = new NetHttpTransport().createRequestFactory(new HttpCredentialsAdapter(idTokenCredentials));

And my user credential is not compliant with IdTokenProvider interface


Caused by: java.lang.ClassCastException: class com.google.auth.oauth2.UserCredentials cannot be cast to class com.google.auth.oauth2.IdTokenProvider (com.google.auth.oauth2.UserCredentials and com.google.auth.oauth2.IdTokenProvider are in unnamed module of loader 'app')

NODE

With node, this code work with a service account

    const {GoogleAuth} = require('google-auth-library');
    const auth = new GoogleAuth()
    const client = await auth.getIdTokenClient(url);
    const res = await client.request({url});
    console.log(res.data);

But with my user account, I got this error

Error: Cannot fetch ID token in this environment, use GCE or set the GOOGLE_APPLICATION_CREDENTIALS environment variable to a service account credentials JSON file.
    at GoogleAuth.getIdTokenClient(....)
guillaume blaquiere
  • 66,369
  • 2
  • 47
  • 76

3 Answers3

4

gcloud auth activate-service-account --key-file is only for "you" running gcloud commands, it won’t be picked up by "applications" that need GOOGLE_APPLICATION_CREDENTIALS.

As you can see from Invoke a Google Cloud Run from java or How to call Cloud Run from out side of Cloud Run/GCP?, you either need to have the JSON key file of Service Account, or have to be running inside a GCE/GKE/Cloud Run/App Engine/GCF instance.

For this to work on your local environment, I recommend logging in with gcloud auth application-default login command (this command is meant to work like as if you’ve set GOOGLE_APPLICATION_CREDENTIALS locally). Let me know if this works.

If that doesn't work, as a last resort you can refactor your code to pick up identity token from an environment variable (if set) while working locally, such as:

$ export ID_TOKEN="$(gcloud auth print-identity-token -q)"
$ ./your-app

Appendix

Per Guillaume's request "how does this gcloud auth print-identity-token work with my personal Google credentials"?

Here's how:

  1. gcloud is an OAuth client registered to Google, to get a JWT token you either need to work inside a GCE/GCF/GKE/GAE/CR environment OR have an OAuth app.

  2. gcloud issues a request like this:

    POST https://www.googleapis.com/oauth2/v4/token
    

    with body:

    grant_type=refresh_token&client_id=REDACTED.apps.googleusercontent.com&client_secret=REDACTED&refresh_token=REDACTED
    

    Most likely client_id and client_secret values are the same for us because they are gcloud’s. However, it exchanges your refresh_token with a new token.

  3. The /token response will contain an access_token and id_token. e.g.:

    {
    "access_token": ".....",
    "id_token": "eyJhbG....."
    "expires_in": 3599,
    "scope": "https://www.googleapis.com/auth/userinfo.email 
    https://www.googleapis.com/auth/appengine.admin 
    https://www.googleapis.com/auth/compute 
    https://www.googleapis.com/auth/cloud-platform 
    https://www.googleapis.com/auth/accounts.reauth openid",
    "token_type": "Bearer",
    }
    

This is how gcloud prints an identity token for you.

If you are curious how does the code do it with a JSON key file, you’ll see something very similar. For example, in Go there's http://google.golang.org/api/idtoken package and you'll see a similar implementation there.

ahmet alp balkan
  • 42,679
  • 38
  • 138
  • 214
  • The problem in refactoring the code is that it doesn't work seamlessly between my local env and GCP env. You are correct about the `activate-service-account`, the env var for credentials was still there (I edited my question for correcting my mistake) – guillaume blaquiere May 12 '20 at 18:14
  • 1
    And I confirm you, the `gcloud auth application-default login` use my personal credential, and it doesn't work – guillaume blaquiere May 12 '20 at 18:14
  • Yeah I'm afraid in this case you actually need full creds (i.e. a service account JSON key). – ahmet alp balkan May 13 '20 at 23:15
  • OK, but in this case, I would like to know what magic stuff performs the command `gcloud auth print-identity-token` for generating a valid token with my user account! – guillaume blaquiere May 14 '20 at 07:31
  • That's complicated, I'm adding an appendix to the question. – ahmet alp balkan May 14 '20 at 21:46
  • Thanks. I was sure that there was an OAuth2 flow, but I didn't know which client_id and client_secret were used. Now it's clear. when I perform a `gcloud auth login` refresh token is stored locally. When I need an identity token, it's used with the client_id and client_secret of Google for getting the identity token. Great, it's now clear for me!! – guillaume blaquiere May 15 '20 at 08:39
2

If you really want to get an Identity Token for a User Account rather than a Service account. You need to already be authenticated with gcloud auth application-default login but then you can do this:

UserCredentials credentials = (UserCredentials) GoogleCredentials.getApplicationDefault();

CloseableHttpClient client = HttpClients.createDefault();
HttpPost httpPost = new HttpPost("https://accounts.google.com/o/oauth2/token");
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("grant_type", "refresh_token"));
params.add(new BasicNameValuePair("client_id", credentials.getClientId()));
params.add(new BasicNameValuePair("client_secret", credentials.getClientSecret()));
params.add(new BasicNameValuePair("refresh_token", credentials.getRefreshToken()));
httpPost.setEntity(new UrlEncodedFormEntity(params));

String id_token = "";
try (CloseableHttpClient c = HttpClientBuilder.create().disableRedirectHandling().build();
    CloseableHttpResponse response = c.execute(httpPost)) {
    String responseString = EntityUtils.toString(response.getEntity(), "UTF-8");
    JsonNode parent= new ObjectMapper().readTree(responseString);
    id_token = parent.path("id_token").asText();
}
return id_token;
Michael Galos
  • 1,065
  • 3
  • 13
  • 27
1

Facing a similar issue but I want code that is portable between local development and GCP environments.

I found @Michael Galos answer to be very effective but ultimately created this method that considers whether a user credential or service account is in-use and acts appropriately.

This is heavily inspired by @Michael's answer but uses the Google Http Client to avoid additional dependencies to request an ID token and re-uses the same token until it expires.

  private GoogleCredentials credentials;
  private IdTokenProvider idToken;
  private long idTokenExpiry;
  

  private IdTokenCredentials getIdTokenCredentials( GoogleCredentials credentials, String url ) throws ParseException, IOException
  {
     if( idToken == null || idTokenExpiry - DateUtils.MILLIS_PER_MINUTE < System.currentTimeMillis() )
     {
        if( credentials instanceof UserCredentials )
        {
           UserCredentials userCredentials = ( UserCredentials )credentials;

           HttpRequest request = transport.createRequestFactory().buildPostRequest(
                    new GenericUrl( "https://accounts.google.com/o/oauth2/token" ),
                    new UrlEncodedContent( Map.of(
                             "grant_type", "refresh_token",
                             "client_id", userCredentials.getClientId(),
                             "client_secret", userCredentials.getClientSecret(),
                             "refresh_token", userCredentials.getRefreshToken() ) ) );

           HttpResponse response = request.execute();
           JsonNode parent = new ObjectMapper().readTree( response.parseAsString() );
           idToken = new IdTokenProvider()
           {

              @Override
              public IdToken idTokenWithAudience( String targetAudience, List<Option> options ) throws IOException
              {
                 idTokenExpiry = System.currentTimeMillis() + ( parent.path( "expires_in" ).asLong() * 1000 );
                 return IdToken.create( parent.path( "id_token" ).asText() );
              }

           };

        }
        else
        {
           idToken = ( IdTokenProvider )credentials;
        }
     }
     return IdTokenCredentials.newBuilder()
              .setIdTokenProvider( idToken )
              .setTargetAudience( url )
              .build();
  }
3urdoch
  • 7,192
  • 8
  • 42
  • 58