Invoking Google Cloud Run service using Workflow Identity Federation

147 Views Asked by At

I'm trying to invoke a service which I have deployed to Google Cloud Run. It is a simple 'hello world' service but with 'require authentication' as the authentication option.

I am trying to call it from an AWS EC2 instance, using the workflow identity federation. I have followed the instructions here https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds (and I also found this helpful) and I have proved that it is successfully impersonating the service account, as this following code running from the EC2 instance works to successfully list the GCP storage buckets:

const {GoogleAuth} = require('google-auth-library');

async function main() {
    const auth = new GoogleAuth({
        scopes: 'https://www.googleapis.com/auth/cloud-platform'
      });
      const client = await auth.getClient();
      const projectId = await auth.getProjectId();
      console.log('projectId = ', projectId)
      // List all buckets in a project.
    const url = `https://storage.googleapis.com/storage/v1/b?project=${projectId}`;
    const res = await client.request({ url });
    console.log(res.data);
  }
  
  main().catch(console.error);

However, when I try to call my Cloud Run service using the following code:

const {GoogleAuth} = require('google-auth-library');

async function main() {
  const targetAudience = 'https://my-cloud-run-service.a.run.app/'
  const auth = new GoogleAuth({
      scopes: 'https://www.googleapis.com/auth/cloud-platform',
      targetAudience: targetAudience
    });
    const client = await auth.getClient();
    const projectId = await auth.getProjectId();
    console.log('projectId = ', projectId)
    
    // it's fine up to here
    const url = targetAudience;

    // this fails with HTTP error 401 unauthorised
    const response = await client.request({url});
    console.log(response);

  }
  
  main().catch(console.error);

then it fails with:

GaxiosError: 
<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>401 Unauthorized</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Unauthorized</h1>
<h2>Your client does not have permission to the requested URL <code>/</code>.</h2>
<h2></h2>
</body></html>

    at Gaxios._request (/home/ubuntu/trader2/node_modules/gaxios/build/src/gaxios.js:141:23)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async AwsClient.requestAsync (/home/ubuntu/trader2/node_modules/google-auth-library/build/src/auth/baseexternalclient.js:246:24)
    at async main (/home/ubuntu/trader2/testAuth.js:15:22)

Now if I set GOOGLE_APPLICATION_CREDENTIALS to the service account key itself, rather than the file downloaded when I grant the service account access to the workflow identity federation, and call getIdTokenClient instead of getClient, then it works fine. This is kind of acceptable, because as long as I protect the service account key file, then it's still secure enough. But I would just like to get the recommended way working if possible - frustrating that there must be one piece of the puzzle I'm missing!

Does anyone know a configuration step I may have missed, anything to test, or if there's anything wrong with my code?

Any help much appreciated

1

There are 1 best solutions below

6
BenTaylor On

Success - I've got it working using the below code.

Many thanks @JohnHanley - this info you provided me about using the access token to exchange for an OIDC access token, the difference between the authorization requirements between google services and invoking user-managed cloud run services, and the endpoint to use to gain the OIDC token, has just provided the missing piece of the puzzle. So thanks again, much appreciated.

However I do think like @guillaume-blaquiere says, it would be nice if this were part of the library.

const {GoogleAuth} = require('google-auth-library');
const axios = require('axios')

async function main() {
  const targetAudience = 'https://my-cloud-run-service-url.run.app/'
  const auth = new GoogleAuth({
      scopes: 'https://www.googleapis.com/auth/cloud-platform',
      targetAudience: targetAudience
    });
    const client = await auth.getClient();
    const accessTokenResponse = await client.getAccessToken();
    const accessToken = accessTokenResponse?.token;
    if(!accessToken) {
      throw new Error("accessToken not present");
    }

    const serviceAccountEmail = client.getServiceAccountEmail()
    const getServiceTokenData = {
      audience: targetAudience
    }
    const getServiceTokenHeaders = {
      'Content-Type': 'text/json',
      'Authorization': `Bearer ${accessToken}`
    }
    const getServiceTokenConfig = {
      headers: getServiceTokenHeaders
    }
    const getServiceTokenResponse = await axios.post(
      `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccountEmail}:generateIdToken`,
      getServiceTokenData, getServiceTokenConfig
    ) // this is simply the call from https://cloud.google.com/docs/authentication/get-id-token#external-idp ,  written in JS

    const idToken = getServiceTokenResponse.data.token;
    const resp = await axios.get(targetAudience, {
      headers: {
        'Authorization': `Bearer ${idToken}`
      }
    })

    console.log(resp.data)

  }
  
  main().catch(console.error);