google:  add support for "impersonated_service_account" credential type.

New credential type supported: "impersonated_service_account".

Extend the "credentialsFile" struct to take into account the credential source for the impersonation.

Reuse of `ImpersonateTokenSource` struct, from `google/internal/externalaccount/Impersonate.go' file. The struct has a package-scope visibility now.

Fixes: #515

Change-Id: I87e213be6d4b6add2d6d82b91b1b38e43a0d2fe4
GitHub-Last-Rev: 14806e6b37a019cbff58e2088ee99191e89b4f7e
GitHub-Pull-Request: golang/oauth2#516
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/344369
Reviewed-by: Cody Oss <codyoss@google.com>
Trust: Cody Oss <codyoss@google.com>
Trust: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Cody Oss <codyoss@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
diff --git a/google/google.go b/google/google.go
index 41ced10..ccc23ee 100644
--- a/google/google.go
+++ b/google/google.go
@@ -92,9 +92,10 @@
 
 // JSON key file types.
 const (
-	serviceAccountKey  = "service_account"
-	userCredentialsKey = "authorized_user"
-	externalAccountKey = "external_account"
+	serviceAccountKey          = "service_account"
+	userCredentialsKey         = "authorized_user"
+	externalAccountKey         = "external_account"
+	impersonatedServiceAccount = "impersonated_service_account"
 )
 
 // credentialsFile is the unmarshalled representation of a credentials file.
@@ -121,9 +122,13 @@
 	TokenURLExternal               string                           `json:"token_url"`
 	TokenInfoURL                   string                           `json:"token_info_url"`
 	ServiceAccountImpersonationURL string                           `json:"service_account_impersonation_url"`
+	Delegates                      []string                         `json:"delegates"`
 	CredentialSource               externalaccount.CredentialSource `json:"credential_source"`
 	QuotaProjectID                 string                           `json:"quota_project_id"`
 	WorkforcePoolUserProject       string                           `json:"workforce_pool_user_project"`
+
+	// Service account impersonation
+	SourceCredentials *credentialsFile `json:"source_credentials"`
 }
 
 func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
@@ -180,6 +185,23 @@
 			WorkforcePoolUserProject:       f.WorkforcePoolUserProject,
 		}
 		return cfg.TokenSource(ctx)
+	case impersonatedServiceAccount:
+		if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
+			return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
+		}
+
+		ts, err := f.SourceCredentials.tokenSource(ctx, params)
+		if err != nil {
+			return nil, err
+		}
+		imp := externalaccount.ImpersonateTokenSource{
+			Ctx:       ctx,
+			URL:       f.ServiceAccountImpersonationURL,
+			Scopes:    params.Scopes,
+			Ts:        ts,
+			Delegates: f.Delegates,
+		}
+		return oauth2.ReuseTokenSource(nil, imp), nil
 	case "":
 		return nil, errors.New("missing 'type' field in credentials")
 	default:
diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go
index a1e36c0..bc3ce53 100644
--- a/google/internal/externalaccount/basecredentials.go
+++ b/google/internal/externalaccount/basecredentials.go
@@ -140,11 +140,11 @@
 	}
 	scopes := c.Scopes
 	ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
-	imp := impersonateTokenSource{
-		ctx:    ctx,
-		url:    c.ServiceAccountImpersonationURL,
-		scopes: scopes,
-		ts:     oauth2.ReuseTokenSource(nil, ts),
+	imp := ImpersonateTokenSource{
+		Ctx:    ctx,
+		URL:    c.ServiceAccountImpersonationURL,
+		Scopes: scopes,
+		Ts:     oauth2.ReuseTokenSource(nil, ts),
 	}
 	return oauth2.ReuseTokenSource(nil, imp), nil
 }
diff --git a/google/internal/externalaccount/impersonate.go b/google/internal/externalaccount/impersonate.go
index 64edb56..8251fc8 100644
--- a/google/internal/externalaccount/impersonate.go
+++ b/google/internal/externalaccount/impersonate.go
@@ -29,30 +29,44 @@
 	ExpireTime  string `json:"expireTime"`
 }
 
-type impersonateTokenSource struct {
-	ctx context.Context
-	ts  oauth2.TokenSource
+// ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL.
+// Scopes can be defined when the access token is requested.
+type ImpersonateTokenSource struct {
+	// Ctx is the execution context of the impersonation process
+	// used to perform http call to the URL. Required
+	Ctx context.Context
+	// Ts is the source credential used to generate a token on the
+	// impersonated service account. Required.
+	Ts oauth2.TokenSource
 
-	url    string
-	scopes []string
+	// URL is the endpoint to call to generate a token
+	// on behalf the service account. Required.
+	URL string
+	// Scopes that the impersonated credential should have. Required.
+	Scopes []string
+	// Delegates are the service account email addresses in a delegation chain.
+	// Each service account must be granted roles/iam.serviceAccountTokenCreator
+	// on the next service account in the chain. Optional.
+	Delegates []string
 }
 
 // Token performs the exchange to get a temporary service account token to allow access to GCP.
-func (its impersonateTokenSource) Token() (*oauth2.Token, error) {
+func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
 	reqBody := generateAccessTokenReq{
-		Lifetime: "3600s",
-		Scope:    its.scopes,
+		Lifetime:  "3600s",
+		Scope:     its.Scopes,
+		Delegates: its.Delegates,
 	}
 	b, err := json.Marshal(reqBody)
 	if err != nil {
 		return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err)
 	}
-	client := oauth2.NewClient(its.ctx, its.ts)
-	req, err := http.NewRequest("POST", its.url, bytes.NewReader(b))
+	client := oauth2.NewClient(its.Ctx, its.Ts)
+	req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b))
 	if err != nil {
 		return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err)
 	}
-	req = req.WithContext(its.ctx)
+	req = req.WithContext(its.Ctx)
 	req.Header.Set("Content-Type", "application/json")
 
 	resp, err := client.Do(req)