Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ja21948 opened this issue Dec 12, 2023 · 5 comments
Assignees
Labels
type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.

Comments

@ja21948
Copy link

ja21948 commented Dec 12, 2023

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.

ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))

@ja21948 ja21948 added priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Dec 12, 2023
@codyoss codyoss assigned quartzmo and unassigned codyoss Dec 13, 2023
@codyoss codyoss added type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design. and removed priority: p2 Moderately-important priority. Fix may not be included in next release. type: bug Error or flaw in code with unintended results or allowing sub-optimal usage patterns. labels Apr 30, 2024
@codyoss
Copy link
Member

codyoss commented Apr 30, 2024

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.

@allan-mercari
Copy link

allan-mercari commented May 1, 2024

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 #1792 and modified in #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.

@quartzmo
Copy link
Member

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

@ericnorris
Copy link

I've encountered this issue with this library, and also submitted an issue in the google-cloud-go repository: googleapis/google-cloud-go#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:

ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature request ‘Nice-to-have’ improvement, new feature or different behavior or design.
Projects
None yet
Development

No branches or pull requests

5 participants