| // 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 |
| // TokenLifetimeSeconds is the number of seconds the impersonation token will |
| // be valid for. |
| TokenLifetimeSeconds int |
| } |
| |
| // Token performs the exchange to get a temporary service account token to allow access to GCP. |
| func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) { |
| lifetimeString := "3600s" |
| if its.TokenLifetimeSeconds != 0 { |
| lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds) |
| } |
| reqBody := generateAccessTokenReq{ |
| Lifetime: lifetimeString, |
| 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 |
| } |