google-api-go-client icon indicating copy to clipboard operation
google-api-go-client copied to clipboard

golang SA impersonation requires permission iam.serviceAccounts.getOpenIdToken whereas other clients do not

Open ja21948 opened this issue 1 year ago • 4 comments

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

ja21948 avatar Dec 12 '23 23:12 ja21948

Go: https://github.com/googleapis/google-api-go-client/blob/10dbf2b5d87783d3dc3de50ea627e740c784137a/idtoken/idtoken.go#L159

PHP: https://github.com/googleapis/google-auth-library-php/blob/999e9ce8b9d17914f04e1718271a0a46da4de2f3/src/Credentials/ImpersonatedServiceAccountCredentials.php#L74

Javascript: https://github.com/googleapis/google-auth-library-nodejs/blob/02e30d4bd781792ee07a345312efe15e689bf134/src/auth/googleauth.ts#L569

quartzmo avatar Dec 19 '23 16:12 quartzmo

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.

codyoss avatar Apr 30 '24 21:04 codyoss

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.

allan-mercari avatar May 01 '24 00:05 allan-mercari

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

quartzmo avatar Jun 13 '24 23:06 quartzmo

I've encountered this issue with this library, and also submitted an issue in the google-cloud-go repository: https://github.com/googleapis/google-cloud-go/issues/11105

I believe the issue is similar here. As noted by others in the thread, the underlying oauth2/google library wraps the credential source in an impersonated credential source (https://github.com/golang/oauth2/blob/22134a41033e44c2cd074106770ab5b7ca910d15/google/externalaccount/basecredentials.go#L245-L257), and then this library wraps it again: https://github.com/googleapis/google-api-go-client/blob/6495d84ae6142aa900e6913fb681ca38ec3a3a6b/idtoken/idtoken.go#L181

ericnorris avatar Nov 08 '24 20:11 ericnorris