Open ja21948 opened 6 months ago
Moving this to a feature request because at the time of implementation impersonated service accounts were not a concept. We will evaluate if we will switch this logic in the future.
I reported this to google cloud support, who created this issue.
I believed this to be a bug in idtoken, because idtoken has special logic to handle issuing tokens using impersonated credentials added two years ago in https://github.com/googleapis/google-api-go-client/pull/1792 and modified in https://github.com/googleapis/google-api-go-client/pull/1897 that differs from the same feature implementation in other google auth libraries.
If this is not a bug it would be very helpful for the expected behavior among the different client libraries to be documented somewhere or the idtoken docs to point to the right pattern. Both me and two separate teammates independently encountered this issue of IAM failures when attempting to use the idtoken library with impersonated credentials, even though we had roles/iam.serviceAccountTokenCreator
on the SA and it worked fine with gcloud
and the other libraries.
I'm attaching code for reproduction below showing that gcloud
cli and the google-auth-library-nodejs do not require an impersonated SA to have getAccessToken
on itself.
Setup a new SA and give yourself serviceAccountTokenCreator
, and showing that we can create an idtoken using the tokeninfo endpoint. It will also set up the ADCs for the next reproduction cases:
#!/bin/bash
# Prerequisites:
# 1. logged into gcloud cli as a regular user (just `gcloud auth login`)
# 2. CLOUDSDK_CORE_PROJECT is set
set -euxo pipefail
SA_NAME="idtoken-2301-$(date +%Y-%m-%d)"
# Just in case, clean up any existing service account
gcloud iam service-accounts delete $SA_NAME@$CLOUDSDK_CORE_PROJECT.iam.gserviceaccount.com --quiet || true
gcloud iam service-accounts create $SA_NAME
# grant ourselves token creator on the service account
gcloud iam service-accounts add-iam-policy-binding $SA_NAME@$CLOUDSDK_CORE_PROJECT.iam.gserviceaccount.com --member=user:$(gcloud config get-value account) --role=roles/iam.serviceAccountTokenCreator
# Check that we can create a token
curl "https://oauth2.googleapis.com/tokeninfo?id_token=$(gcloud auth print-identity-token --include-email --impersonate-service-account=$SA_NAME@$CLOUDSDK_CORE_PROJECT.iam.gserviceaccount.com)"
# ADCs to use the new service account
gcloud auth application-default login --impersonate-service-account=$SA_NAME@$CLOUDSDK_CORE_PROJECT.iam.gserviceaccount.com
main.go
:
package main
import (
"context"
"fmt"
"google.golang.org/api/idtoken"
)
func main() {
ts, err := idtoken.NewTokenSource(context.Background(), "https://example.com")
if err != nil {
panic(err)
}
token, err := ts.Token()
if err != nil {
panic(err)
}
fmt.Println(token)
}
Run with:
GODEBUG=http2debug=2 go run ./main.go 2>&1 | grep -E ':path|message'
This will output the following, showing that the service account is attempting to issue an idToken for itself:
2024/05/01 09:27:02 http2: Transport encoding header ":path" = "/token"
2024/05/01 09:27:03 http2: Transport encoding header ":path" = "/v1/projects/-/serviceAccounts/idtoken-2301-2024-05-01@[REDACTED].gserviceaccount.com:generateAccessToken"
2024/05/01 09:27:03 http2: Transport encoding header ":path" = "/v1/projects/-/serviceAccounts/idtoken-2301-2024-05-01@[REDACTED].iam.gserviceaccount.com:generateIdToken"
"message": "Permission 'iam.serviceAccounts.getOpenIdToken' denied on resource (or it may not exist).",
However, compare with this node code:
const {GoogleAuth} = require('google-auth-library');
// https://github.com/googleapis/google-auth-library-nodejs/blob/6014adec1b7b1e9abe6fa2fdd53e3231029f9129/samples/idTokenFromMetadataServer.js#L34
async function main() {
const auth = new GoogleAuth();
const client = await auth.getIdTokenClient("https://example.com");
const token = await client.idTokenProvider.fetchIdToken("https://example.com");
const r = await fetch("https://oauth2.googleapis.com/tokeninfo?id_token=" + token);
console.log(await r.json());
}
main().catch(console.error);
Running this with node test.js
will output the token info for the service account.
The google.golang.org/api/idtoken
package is being replaced by the cloud.google.com/go/auth/credentials/idtoken
package. We may want to move this issue to the googleapis/google-cloud-go repo. The equivalent logic is at: https://github.com/googleapis/google-cloud-go/blob/auth/v0.5.1/auth/credentials/idtoken/file.go#L113
The golang idtoken library first calls generateAccessToken on the impersonated service account as the source user, and then uses that access token to call generateIdToken on the service account. This requires the service account to have the permission of iam.serviceAccounts.getOpenIdToken access on itself.
The issue is that the idtoken library [in Go lang] does not use the source_credentials subfield in the JSON struct when constructing the inner client, and instead uses the entire credential json. The other clients (like JS and PHP clients) do not operate in this way.
https://github.com/googleapis/google-api-go-client/blob/10dbf2b5d87783d3dc3de50ea627e740c784137a/idtoken/idtoken.go#L159