Generate JWT for service account using compute metadata from cloud function

1.3k views Asked by At

I'm trying to generate a JWT for a given service account serviceA from a Google/Firebase Cloud function. Service account serviceB is running the function.

I got it working by using the account keys from JSON.

Given that the CF is running within Google Cloud, I want to leverage compute metadata to not having to store the private key with the functions.

I've been trying to access the metadata server for serviceA while serviceB is executing the CF. I deliberately don't want serviceA to run the CF.

The code

const request = require('request-promise');
const serviceAccountEmail = 'serviceA@<projectA>.iam.gserviceaccount.com';
const metadataServerTokenURL = `http://metadata/computeMetadata/v1/instance/service-accounts/${serviceAccountEmail}/identity?audience=<audience>`;
const tokenRequestOptions = {
    uri: metadataServerTokenURL,
    headers: {
        'Metadata-Flavor': 'Google'
    }
};
const token = await request(tokenRequestOptions);

The error

I'm currently getting a 404 not found error for the email provided

I guess it's
a) not possible what I'm trying to do, or
b) I'm missing some IAM permissions for serviceA

2

There are 2 answers

1
guillaume blaquiere On BEST ANSWER

You can do this with the metadata server because they can only generate ID Token for the service account loaded with your instance (in this case the serviceB).

You can use another API for this: Service Account Credentials API, especially the generateIdToken method

In your case, you can do something like this (in python here)

import google.auth
from google.auth.transport.requests import AuthorizedSession
import json


# IAP audience is the ClientID of IAP-App-Engine-app in 
# the API->credentials page
# Cloud Function and Cloud Run need the base URL of the service
audience = 'YOUR AUDIENCE'
# #1 Get the default credential to generate the access token
credentials, project_id = google.auth.default(
            scopes='https://www.googleapis.com/auth/cloud-platform')

# #2 To use the current service account email
service_account_email = credentials.service_account_email
# Don't work with user account, so define manually the email
# service_account_email = 'MY SERVICE ACCOUNT EMAIL'
# #3 prepare the call the the service account credentials API
sa_credentials_url =  f'https://iamcredentials.googleapis.com/' \
                      f'v1/projects/-/serviceAccounts/'  \
                      f'{service_account_email}:generateIdToken'
headers = {'Content-Type': 'application/json'}

# Create an AuthorizedSession that includes 
# automatically the access_token based on your credentials
authed_session = AuthorizedSession(credentials)
# Define the audience in the request body
# add the parameter "'includeEmail':true" for IAP access
body = json.dumps({'audience': audience})
# Make the call 
token_response = authed_session.request('POST',sa_credentials_url,
                                        data=body, headers=headers)

jwt = token_response.json()
id_token = jwt['token']

I wrote an article on this, this week

1
bhr On

I adopted @guillaume blaquiere's solution to Typescript:

import { GaxiosOptions, Headers } from 'gaxios';
import { GoogleAuth } from 'google-auth-library';

interface TokenRequestResponse {
  token: string
}

const service_account_email = 'MY SERVICE ACCOUNT EMAIL'
const audience: string = 'MY AUDIENCE';
const sa_credentials_url = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${service_account_email}:generateIdToken`

const headers: Headers = { 'Content-Type': 'application/json' }
const body = {
  'audience':  audience
};
const options: GaxiosOptions = {
  method: 'POST',
  url: sa_credentials_url,
  body: JSON.stringify(body),
  headers: headers,
};

const auth = new GoogleAuth({
  scopes: 'https://www.googleapis.com/auth/cloud-platform'
});
const client = await auth.getClient();
const tokenResponse = await client.request(options);
const tokenRequestResponse = tokenResponse.data as TokenRequestResponse;
const token = tokenRequestResponse.token;