0
votes

What I'm trying to do is generate a JWT for calling the HTTP trigger on a GCP Cloud Function I've deployed. I've already deployed my function with 'allUsers' and verified it works, but I want it to be more secure, so I need to attach a JWT to my HTTP request. I'm following this code and the following snippets are mostly from that. I think I am close, but not quite there yet. In all of the samples etc below I've changed my project name to PROJECT_NAME.

First I created a service account named testharness-sa and downloaded its key file. I have an env var GOOGLE_APPLICATION_CREDENTIALS pointing to that file when I run my tests. Then I ran the following command:

gcloud functions add-iam-policy-binding gopubsub \
  --member='serviceAccount:testharness-sa@PROJECT_NAME.iam.gserviceaccount.com' \
  --role='roles/cloudfunctions.invoker'

This gave me a confirmation by listing all the current bindings on my cloud function, including testharness-sa.

The core of my code is this:

    private String getSignedJwt() {
        GoogleCredentials credentials = GoogleCredentials
                .getApplicationDefault()
                .createScoped(Collections.singleton(IAM_SCOPE));
        long now = System.currentTimeMillis();
        RSAPrivateKey privateKey = (RSAPrivateKey) credentials.getPrivateKey();
        Algorithm algorithm = Algorithm.RSA256(null, privateKey);
        return JWT.create()
                .withKeyId(credentials.getPrivateKeyId())
                .withIssuer(credentials.getClientEmail())
                .withSubject(credentials.getClientEmail())
                .withAudience(OAUTH_TOKEN_AUDIENCE)
                .withIssuedAt(new Date(now))
                .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
                .withClaim("target_audience", clientId)
                .sign(algorithm);
    }

This gives me a signed JWT. As I understand things, this is used to call GCP to get me a final JWT I can use to call my cloud function.

Once I generate the signed JWT I use it like this:

        String jwt = getSignedJwt();
        final GenericData tokenRequest = new GenericData()
                .set("grant_type", JWT_BEARER_TOKEN_GRANT_TYPE)
                .set("assertion", jwt);
        final UrlEncodedContent content = new UrlEncodedContent(tokenRequest);

        final HttpRequestFactory requestFactory = httpTransport.createRequestFactory();

        final HttpRequest request = requestFactory
                .buildPostRequest(new GenericUrl(OAUTH_TOKEN_URI), content)
                .setParser(new JsonObjectParser(JacksonFactory.getDefaultInstance()));

        HttpResponse response = request.execute();
...

So it gets the signed JWT and makes up a request to get the final JWT and then... fails. The error is "Invalid JWT: Failed audience check" It looks like I have a bad parameter, so let's look at my parameters (they are actually constants, not parameters):

    private static final String IAM_SCOPE = "https://www.googleapis.com/auth/iam";
    //"https://www.googleapis.com/auth/cloud-platform";
    private static final String OAUTH_TOKEN_URI = "https://oauth2.googleapis.com/token";
    //"https://www.googleapis.com/oauth2/v4/token";
    private static final String OAUTH_TOKEN_AUDIENCE = "https://us-central1-PROJECT_NAME.cloudfunctions.net/gopubsub";
    //"https://www.googleapis.com/token";
    private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

I've added other variants as comments against the constants.

Based on the error message it looks like the audience value I'm using is wrong. This page suggests the audience should be the service account email, and I think I've seen elsewhere that it ought to be the URL of the cloud function, but neither of those work for me.

I do know this can work because I can issue this command:

gcloud auth print-identity-token

which gives me the final JWT (as long as I have GOOGLE_APPLICATION_CREDENTIALS pointing at my json file). I can paste that JWT into a curl command and invoke the HTTP trigger successfully, and if I leave the JWT out it fails, so I know the JWT is being checked. But so far I don't know how to do the equivalent of gcloud auth print-identity-token from my Java code. Anyone know?

1
Have you tried generating the jwt without the IAM_SCOPE as shown in the sample from this link?bhito
I have now :) and yes, it doesn't seem to make any difference, however I found the problem wasn't related to scope. See my answer below.RogerParkinson

1 Answers

0
votes

I found the answer was not related to scope, despite the error message. It is actually related to the clientId value I was passing (and didn't really mention in my question, though it is there in the code). I was using a value I found in the service account json file, a really long string of digits. That was the problem. It needed to be the URL of my HTTP trigger. So these are the constants I ended up with:

    private static final String IAM_SCOPE = "https://www.googleapis.com/auth/cloud-platform";
    private static final String OAUTH_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
    private static final String OAUTH_TOKEN_AUDIENCE = "https://www.googleapis.com/oauth2/v4/token";
    private static final String JWT_BEARER_TOKEN_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer";
    private static final long EXPIRATION_TIME_IN_MILLIS = 3600 * 1000L;

In fact it still works if I remove all references to IAM_SCOPE, so I conclude that doesn't matter. The piece of code that pulls it together now looks like this:

        return JWT.create()
                .withKeyId(credentials.getPrivateKeyId())
                .withIssuer(credentials.getClientEmail())
                .withSubject(credentials.getClientEmail())
                .withAudience(OAUTH_TOKEN_AUDIENCE)
                .withIssuedAt(new Date(now))
                .withExpiresAt(new Date(now + EXPIRATION_TIME_IN_MILLIS))
                .withClaim("target_audience", targetURL)
                .sign(algorithm);

Specifically the change is the withClaim() call which now contains the URL I want to call. With that in place the code returns the JWT I was hoping for and which does, in fact, work when I call the secured HTTP trigger URL. Hope that helps someone else.