1
18
19
20
21
22
23
24
25
26 package sts
27
28 import (
29 "bytes"
30 "context"
31 "crypto/tls"
32 "crypto/x509"
33 "encoding/json"
34 "errors"
35 "fmt"
36 "io"
37 "net/http"
38 "net/url"
39 "os"
40 "sync"
41 "time"
42
43 "google.golang.org/grpc/credentials"
44 "google.golang.org/grpc/grpclog"
45 )
46
47 const (
48
49 stsRequestTimeout = 5 * time.Second
50
51
52 minCachedTokenLifetime = 300 * time.Second
53
54 tokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
55 defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
56 )
57
58
59 var (
60 loadSystemCertPool = x509.SystemCertPool
61 makeHTTPDoer = makeHTTPClient
62 readSubjectTokenFrom = os.ReadFile
63 readActorTokenFrom = os.ReadFile
64 logger = grpclog.Component("credentials")
65 )
66
67
68 type Options struct {
69
70
71 TokenExchangeServiceURI string
72
73
74
75 Resource string
76
77
78
79 Audience string
80
81
82
83
84
85
86 Scope string
87
88
89
90
91 RequestedTokenType string
92
93
94
95
96 SubjectTokenPath string
97
98
99
100
101 SubjectTokenType string
102
103
104
105 ActorTokenPath string
106
107
108
109
110 ActorTokenType string
111 }
112
113 func (o Options) String() string {
114 return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s:%s", o.TokenExchangeServiceURI, o.Resource, o.Audience, o.Scope, o.RequestedTokenType, o.SubjectTokenPath, o.SubjectTokenType, o.ActorTokenPath, o.ActorTokenType)
115 }
116
117
118
119 func NewCredentials(opts Options) (credentials.PerRPCCredentials, error) {
120 if err := validateOptions(opts); err != nil {
121 return nil, err
122 }
123
124
125
126 roots, err := loadSystemCertPool()
127 if err != nil {
128 return nil, err
129 }
130
131 return &callCreds{
132 opts: opts,
133 client: makeHTTPDoer(roots),
134 }, nil
135 }
136
137
138
139 type callCreds struct {
140 opts Options
141 client httpDoer
142
143
144
145 mu sync.Mutex
146 tokenMetadata map[string]string
147 tokenExpiry time.Time
148 }
149
150
151
152 func (c *callCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
153 ri, _ := credentials.RequestInfoFromContext(ctx)
154 if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
155 return nil, fmt.Errorf("unable to transfer STS PerRPCCredentials: %v", err)
156 }
157
158
159
160
161 c.mu.Lock()
162 defer c.mu.Unlock()
163
164 if md := c.cachedMetadata(); md != nil {
165 return md, nil
166 }
167 req, err := constructRequest(ctx, c.opts)
168 if err != nil {
169 return nil, err
170 }
171 respBody, err := sendRequest(c.client, req)
172 if err != nil {
173 return nil, err
174 }
175 ti, err := tokenInfoFromResponse(respBody)
176 if err != nil {
177 return nil, err
178 }
179 c.tokenMetadata = map[string]string{"Authorization": fmt.Sprintf("%s %s", ti.tokenType, ti.token)}
180 c.tokenExpiry = ti.expiryTime
181 return c.tokenMetadata, nil
182 }
183
184
185
186 func (c *callCreds) RequireTransportSecurity() bool {
187 return true
188 }
189
190
191
192 type httpDoer interface {
193 Do(req *http.Request) (*http.Response, error)
194 }
195
196 func makeHTTPClient(roots *x509.CertPool) httpDoer {
197 return &http.Client{
198 Timeout: stsRequestTimeout,
199 Transport: &http.Transport{
200 TLSClientConfig: &tls.Config{
201 RootCAs: roots,
202 },
203 },
204 }
205 }
206
207
208
209
210
211 func validateOptions(opts Options) error {
212 if opts.TokenExchangeServiceURI == "" {
213 return errors.New("empty token_exchange_service_uri in options")
214 }
215 u, err := url.Parse(opts.TokenExchangeServiceURI)
216 if err != nil {
217 return err
218 }
219 if u.Scheme != "http" && u.Scheme != "https" {
220 return fmt.Errorf("scheme is not supported: %q. Only http(s) is supported", u.Scheme)
221 }
222
223 if opts.SubjectTokenPath == "" {
224 return errors.New("required field SubjectTokenPath is not specified")
225 }
226 if opts.SubjectTokenType == "" {
227 return errors.New("required field SubjectTokenType is not specified")
228 }
229 return nil
230 }
231
232
233
234
235
236 func (c *callCreds) cachedMetadata() map[string]string {
237 now := time.Now()
238
239
240
241 if c.tokenExpiry.After(now) && c.tokenExpiry.Sub(now) > minCachedTokenLifetime {
242 return c.tokenMetadata
243 }
244 return nil
245 }
246
247
248
249
250
251
252
253
254
255
256
257
258
259 func constructRequest(ctx context.Context, opts Options) (*http.Request, error) {
260 subToken, err := readSubjectTokenFrom(opts.SubjectTokenPath)
261 if err != nil {
262 return nil, err
263 }
264 reqScope := opts.Scope
265 if reqScope == "" {
266 reqScope = defaultCloudPlatformScope
267 }
268 reqParams := &requestParameters{
269 GrantType: tokenExchangeGrantType,
270 Resource: opts.Resource,
271 Audience: opts.Audience,
272 Scope: reqScope,
273 RequestedTokenType: opts.RequestedTokenType,
274 SubjectToken: string(subToken),
275 SubjectTokenType: opts.SubjectTokenType,
276 }
277 if opts.ActorTokenPath != "" {
278 actorToken, err := readActorTokenFrom(opts.ActorTokenPath)
279 if err != nil {
280 return nil, err
281 }
282 reqParams.ActorToken = string(actorToken)
283 reqParams.ActorTokenType = opts.ActorTokenType
284 }
285 jsonBody, err := json.Marshal(reqParams)
286 if err != nil {
287 return nil, err
288 }
289 req, err := http.NewRequestWithContext(ctx, "POST", opts.TokenExchangeServiceURI, bytes.NewBuffer(jsonBody))
290 if err != nil {
291 return nil, fmt.Errorf("failed to create http request: %v", err)
292 }
293 req.Header.Set("Content-Type", "application/json")
294 return req, nil
295 }
296
297 func sendRequest(client httpDoer, req *http.Request) ([]byte, error) {
298
299
300
301
302 resp, err := client.Do(req)
303 if err != nil {
304 return nil, err
305 }
306
307
308
309
310 body, err := io.ReadAll(resp.Body)
311 resp.Body.Close()
312 if err != nil {
313 return nil, err
314 }
315
316 if resp.StatusCode == http.StatusOK {
317 return body, nil
318 }
319 logger.Warningf("http status %d, body: %s", resp.StatusCode, string(body))
320 return nil, fmt.Errorf("http status %d, body: %s", resp.StatusCode, string(body))
321 }
322
323 func tokenInfoFromResponse(respBody []byte) (*tokenInfo, error) {
324 respData := &responseParameters{}
325 if err := json.Unmarshal(respBody, respData); err != nil {
326 return nil, fmt.Errorf("json.Unmarshal(%v): %v", respBody, err)
327 }
328 if respData.AccessToken == "" {
329 return nil, fmt.Errorf("empty accessToken in response (%v)", string(respBody))
330 }
331 return &tokenInfo{
332 tokenType: respData.TokenType,
333 token: respData.AccessToken,
334 expiryTime: time.Now().Add(time.Duration(respData.ExpiresIn) * time.Second),
335 }, nil
336 }
337
338
339
340 type requestParameters struct {
341
342
343 GrantType string `json:"grant_type"`
344
345
346 Resource string `json:"resource,omitempty"`
347
348
349 Audience string `json:"audience,omitempty"`
350
351
352
353 Scope string `json:"scope,omitempty"`
354
355 RequestedTokenType string `json:"requested_token_type,omitempty"`
356
357
358 SubjectToken string `json:"subject_token"`
359
360
361 SubjectTokenType string `json:"subject_token_type"`
362
363
364 ActorToken string `json:"actor_token,omitempty"`
365
366
367 ActorTokenType string `json:"actor_token_type,omitempty"`
368 }
369
370
371
372
373 type responseParameters struct {
374
375
376 AccessToken string `json:"access_token"`
377
378 IssuedTokenType string `json:"issued_token_type"`
379
380
381
382 TokenType string `json:"token_type"`
383
384
385 ExpiresIn int64 `json:"expires_in"`
386
387
388 Scope string `json:"scope"`
389
390
391
392 RefreshToken string `json:"refresh_token"`
393 }
394
395
396 type tokenInfo struct {
397 tokenType string
398 token string
399 expiryTime time.Time
400 }
401
View as plain text