// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package externalaccount import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "time" "golang.org/x/oauth2" ) // generateAccesstokenReq is used for service account impersonation type generateAccessTokenReq struct { Delegates []string `json:"delegates,omitempty"` Lifetime string `json:"lifetime,omitempty"` Scope []string `json:"scope,omitempty"` } type impersonateTokenResponse struct { AccessToken string `json:"accessToken"` ExpireTime string `json:"expireTime"` } // 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 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) { reqBody := generateAccessTokenReq{ 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)) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err) } req = req.WithContext(its.Ctx) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err) } if c := resp.StatusCode; c < 200 || c > 299 { return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body) } var accessTokenResp impersonateTokenResponse if err := json.Unmarshal(body, &accessTokenResp); err != nil { return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err) } expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime) if err != nil { return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err) } return &oauth2.Token{ AccessToken: accessTokenResp.AccessToken, Expiry: expiry, TokenType: "Bearer", }, nil }