1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package impersonate
16
17 import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "net/http"
24 "time"
25
26 "cloud.google.com/go/auth"
27 "cloud.google.com/go/auth/internal"
28 )
29
30 const (
31 defaultTokenLifetime = "3600s"
32 authHeaderKey = "Authorization"
33 )
34
35
36 type generateAccessTokenReq struct {
37 Delegates []string `json:"delegates,omitempty"`
38 Lifetime string `json:"lifetime,omitempty"`
39 Scope []string `json:"scope,omitempty"`
40 }
41
42 type impersonateTokenResponse struct {
43 AccessToken string `json:"accessToken"`
44 ExpireTime string `json:"expireTime"`
45 }
46
47
48
49 func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
50 if err := opts.validate(); err != nil {
51 return nil, err
52 }
53 return opts, nil
54 }
55
56
57 type Options struct {
58
59
60 Tp auth.TokenProvider
61
62
63
64 URL string
65
66 Scopes []string
67
68
69
70 Delegates []string
71
72
73 TokenLifetimeSeconds int
74
75
76 Client *http.Client
77 }
78
79 func (o *Options) validate() error {
80 if o.Tp == nil {
81 return errors.New("credentials: missing required 'source_credentials' field in impersonated credentials")
82 }
83 if o.URL == "" {
84 return errors.New("credentials: missing required 'service_account_impersonation_url' field in impersonated credentials")
85 }
86 return nil
87 }
88
89
90 func (o *Options) Token(ctx context.Context) (*auth.Token, error) {
91 lifetime := defaultTokenLifetime
92 if o.TokenLifetimeSeconds != 0 {
93 lifetime = fmt.Sprintf("%ds", o.TokenLifetimeSeconds)
94 }
95 reqBody := generateAccessTokenReq{
96 Lifetime: lifetime,
97 Scope: o.Scopes,
98 Delegates: o.Delegates,
99 }
100 b, err := json.Marshal(reqBody)
101 if err != nil {
102 return nil, fmt.Errorf("credentials: unable to marshal request: %w", err)
103 }
104 req, err := http.NewRequestWithContext(ctx, "POST", o.URL, bytes.NewReader(b))
105 if err != nil {
106 return nil, fmt.Errorf("credentials: unable to create impersonation request: %w", err)
107 }
108 req.Header.Set("Content-Type", "application/json")
109 if err := setAuthHeader(ctx, o.Tp, req); err != nil {
110 return nil, err
111 }
112 resp, err := o.Client.Do(req)
113 if err != nil {
114 return nil, fmt.Errorf("credentials: unable to generate access token: %w", err)
115 }
116 defer resp.Body.Close()
117 body, err := internal.ReadAll(resp.Body)
118 if err != nil {
119 return nil, fmt.Errorf("credentials: unable to read body: %w", err)
120 }
121 if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
122 return nil, fmt.Errorf("credentials: status code %d: %s", c, body)
123 }
124
125 var accessTokenResp impersonateTokenResponse
126 if err := json.Unmarshal(body, &accessTokenResp); err != nil {
127 return nil, fmt.Errorf("credentials: unable to parse response: %w", err)
128 }
129 expiry, err := time.Parse(time.RFC3339, accessTokenResp.ExpireTime)
130 if err != nil {
131 return nil, fmt.Errorf("credentials: unable to parse expiry: %w", err)
132 }
133 return &auth.Token{
134 Value: accessTokenResp.AccessToken,
135 Expiry: expiry,
136 Type: internal.TokenTypeBearer,
137 }, nil
138 }
139
140 func setAuthHeader(ctx context.Context, tp auth.TokenProvider, r *http.Request) error {
141 t, err := tp.Token(ctx)
142 if err != nil {
143 return err
144 }
145 typ := t.Type
146 if typ == "" {
147 typ = internal.TokenTypeBearer
148 }
149 r.Header.Set(authHeaderKey, typ+" "+t.Value)
150 return nil
151 }
152
View as plain text