I had a similar problem, where I needed to have the application default credentials of a Cloud Run service, impersonate another service account and then this second service account will have the rights for domain wide delegation and impersonate a specific google workspace account. The nodejs library doesn't have this out of the box so I thought to share as figuring the solution was tough.
In essence, you've already managed to create an auth client in your nodejs app that will work with workload identity. I guess it's as simple as that:
const auth = new GoogleAuth({
projectId: 'my-gcp-project-id'
})
const sourceClient = await auth.getClient()
The next step is to create an ID token with the desired scopes and it's subject set as the email address of the google workspace user to impersonate.
While composing the payload of such a token is straightforward, you need to get it signed. For this purpose there is the signJWT method in the iam credentials api.
const payload = {
iss: serviceAccount,// service account e-mail
scope, // space separated list of scopes
aud: 'https://oauth2.googleapis.com/token',
iat,
exp: iat + lifetime,// max 3600
sub: subject // google workspace email
}
const name = `projects/-/serviceAccounts/${serviceAccount}`
const singJwtTokenUrl = `https://iamcredentials.googleapis.com/v1/${name}:signJwt`
// sign JWT token.
const { data: { signedJwt } } = await sourceClient.request({
url: singJwtTokenUrl,
data: {
delegates: [],
payload: JSON.stringify(payload)
},
method: 'POST'
})
Now the next step is to use this signedJWT and get an accessToken, and then create a new oauth client with this access token as credentials.
This auth client can then be used for about an hour, as this is the max lifetime of the access token.
See what worked for me in this helper function:
const getImpersonatedClientForScopes = async ({
serviceAccount, // service account to impersonate
targetScopes,
subject,
sourceClient,
lifetime = 3600 // the Google default for the access token lifetime is max 1 hour
}) => {
if (!subject) {
return new Impersonated({
sourceClient,
targetPrincipal: serviceAccount,
lifetime,
delegates: [],
targetScopes
})
}
// Domain-wide-Delegation
const iat = parseInt(Date.now() / 1000)
const scope = targetScopes.join(' ')
const payload = {
iss: serviceAccount,
scope,
aud: 'https://oauth2.googleapis.com/token',
iat,
exp: iat + lifetime,
sub: subject
}
const name = `projects/-/serviceAccounts/${serviceAccount}`
const singJwtTokenUrl = `https://iamcredentials.googleapis.com/v1/${name}:signJwt`
// sign JWT token.
const { data: { signedJwt } } = await sourceClient.request({
url: singJwtTokenUrl,
data: {
delegates: [],
payload: JSON.stringify(payload)
},
method: 'POST'
})
const {
data: {
access_token,
expires_in,
token_type
}
} = await sourceClient.request({
method: 'POST',
url: 'https://www.googleapis.com/oauth2/v4/token',
data: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: signedJwt
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'json'
})
const credentials = {
// The time in ms at which this token is thought to expire.
expiry_date: expires_in === null || expires_in === undefined ? undefined : (iat + expires_in) * 1000,
// A token that can be sent to a Google API.
access_token,
// Identifies the type of token returned. At this time, this field always has the value Bearer.
token_type,
// A JWT that contains identity information about the user that is digitally signed by Google.
id_token: signedJwt,
// The scopes of access granted by the access_token expressed as a list of space-delimited, case-sensitive strings.
scope: targetScopes.join(' ')
}
const impersonatedClient = new OAuth2Client()
impersonatedClient.setCredentials(credentials)
return impersonatedClient
}
Now the disadvantages of this approach is that it won't perform, as you call two more http endpoints, while when using credential files, the signing happens locally because we have the private key in hand. It might be a good idea to cache the impersonated access token for it's lifetime, but this might not scale if there is a requirement to impersonate multiple users frequently due to rate limits.