googleapis / google-api-go-client

Auto-generated Google APIs for Go.
https://pkg.go.dev/google.golang.org/api
BSD 3-Clause "New" or "Revised" License
3.89k stars 1.17k forks source link

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

Open ja21948 opened 6 months ago

ja21948 commented 6 months ago

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

quartzmo commented 6 months ago

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

codyoss commented 2 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.

allan-mercari commented 2 months ago

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.

quartzmo commented 3 weeks ago

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