1 // Copyright 2021 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4 5 package impersonate
6 7 import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "fmt"
12 "io"
13 "net/http"
14 "time"
15 16 "golang.org/x/oauth2"
17 )
18 19 // generateAccesstokenReq is used for service account impersonation
20 type generateAccessTokenReq struct {
21 Delegates []string `json:"delegates,omitempty"`
22 Lifetime string `json:"lifetime,omitempty"`
23 Scope []string `json:"scope,omitempty"`
24 }
25 26 type impersonateTokenResponse struct {
27 AccessToken string `json:"accessToken"`
28 ExpireTime string `json:"expireTime"`
29 }
30 31 // ImpersonateTokenSource uses a source credential, stored in Ts, to request an access token to the provided URL.
32 // Scopes can be defined when the access token is requested.
33 type ImpersonateTokenSource struct {
34 // Ctx is the execution context of the impersonation process
35 // used to perform http call to the URL. Required
36 Ctx context.Context
37 // Ts is the source credential used to generate a token on the
38 // impersonated service account. Required.
39 Ts oauth2.TokenSource
40 41 // URL is the endpoint to call to generate a token
42 // on behalf the service account. Required.
43 URL string
44 // Scopes that the impersonated credential should have. Required.
45 Scopes []string
46 // Delegates are the service account email addresses in a delegation chain.
47 // Each service account must be granted roles/iam.serviceAccountTokenCreator
48 // on the next service account in the chain. Optional.
49 Delegates []string
50 // TokenLifetimeSeconds is the number of seconds the impersonation token will
51 // be valid for.
52 TokenLifetimeSeconds int
53 }
54 55 // Token performs the exchange to get a temporary service account token to allow access to GCP.
56 func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
57 lifetimeString := "3600s"
58 if its.TokenLifetimeSeconds != 0 {
59 lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds)
60 }
61 reqBody := generateAccessTokenReq{
62 Lifetime: lifetimeString,
63 Scope: its.Scopes,
64 Delegates: its.Delegates,
65 }
66 b, err := json.Marshal(reqBody)
67 if err != nil {
68 return nil, fmt.Errorf("oauth2/google: unable to marshal request: %v", err)
69 }
70 client := oauth2.NewClient(its.Ctx, its.Ts)
71 req, err := http.NewRequest("POST", its.URL, bytes.NewReader(b))
72 if err != nil {
73 return nil, fmt.Errorf("oauth2/google: unable to create impersonation request: %v", err)
74 }
75 req = req.WithContext(its.Ctx)
76 req.Header.Set("Content-Type", "application/json")
77 78 resp, err := client.Do(req)
79 if err != nil {
80 return nil, fmt.Errorf("oauth2/google: unable to generate access token: %v", err)
81 }
82 defer resp.Body.Close()
83 body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
84 if err != nil {
85 return nil, fmt.Errorf("oauth2/google: unable to read body: %v", err)
86 }
87 if c := resp.StatusCode; c < 200 || c > 299 {
88 return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
89 }
90 91 var accessTokenResp impersonateTokenResponse
92 if err := json.Unmarshal(body, &accessTokenResp); err != nil {
93 return nil, fmt.Errorf("oauth2/google: unable to parse response: %v", err)
94 }
95 expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
96 if err != nil {
97 return nil, fmt.Errorf("oauth2/google: unable to parse expiry: %v", err)
98 }
99 return &oauth2.Token{
100 AccessToken: accessTokenResp.AccessToken,
101 Expiry: expiry,
102 TokenType: "Bearer",
103 }, nil
104 }
105