1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package auth
16
17 import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "net/http"
23 "net/url"
24 "strings"
25 "sync"
26 "time"
27
28 "cloud.google.com/go/auth/internal"
29 "cloud.google.com/go/auth/internal/jwt"
30 )
31
32 const (
33
34 codeChallengeKey = "code_challenge"
35 codeChallengeMethodKey = "code_challenge_method"
36
37
38 codeVerifierKey = "code_verifier"
39
40
41
42 defaultExpiryDelta = 215 * time.Second
43
44 universeDomainDefault = "googleapis.com"
45 )
46
47 var (
48 defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
49 defaultHeader = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
50
51
52 timeNow = time.Now
53 )
54
55
56 type TokenProvider interface {
57
58
59
60
61
62
63 Token(context.Context) (*Token, error)
64 }
65
66
67
68 type Token struct {
69
70
71 Value string
72
73
74 Type string
75
76 Expiry time.Time
77
78
79 Metadata map[string]interface{}
80 }
81
82
83
84
85 func (t *Token) IsValid() bool {
86 return t.isValidWithEarlyExpiry(defaultExpiryDelta)
87 }
88
89 func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
90 if t == nil || t.Value == "" {
91 return false
92 }
93 if t.Expiry.IsZero() {
94 return true
95 }
96 return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
97 }
98
99
100
101 type Credentials struct {
102 json []byte
103 projectID CredentialsPropertyProvider
104 quotaProjectID CredentialsPropertyProvider
105
106 universeDomain CredentialsPropertyProvider
107
108 TokenProvider
109 }
110
111
112
113 func (c *Credentials) JSON() []byte {
114 return c.json
115 }
116
117
118
119 func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
120 if c.projectID == nil {
121 return internal.GetProjectID(c.json, ""), nil
122 }
123 v, err := c.projectID.GetProperty(ctx)
124 if err != nil {
125 return "", err
126 }
127 return internal.GetProjectID(c.json, v), nil
128 }
129
130
131
132 func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
133 if c.quotaProjectID == nil {
134 return internal.GetQuotaProject(c.json, ""), nil
135 }
136 v, err := c.quotaProjectID.GetProperty(ctx)
137 if err != nil {
138 return "", err
139 }
140 return internal.GetQuotaProject(c.json, v), nil
141 }
142
143
144
145 func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
146 if c.universeDomain == nil {
147 return universeDomainDefault, nil
148 }
149 v, err := c.universeDomain.GetProperty(ctx)
150 if err != nil {
151 return "", err
152 }
153 if v == "" {
154 return universeDomainDefault, nil
155 }
156 return v, err
157 }
158
159
160
161 type CredentialsPropertyProvider interface {
162 GetProperty(context.Context) (string, error)
163 }
164
165
166
167 type CredentialsPropertyFunc func(context.Context) (string, error)
168
169
170 func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
171 return p(ctx)
172 }
173
174
175 type CredentialsOptions struct {
176
177 TokenProvider TokenProvider
178
179 JSON []byte
180
181
182 ProjectIDProvider CredentialsPropertyProvider
183
184
185 QuotaProjectIDProvider CredentialsPropertyProvider
186
187 UniverseDomainProvider CredentialsPropertyProvider
188 }
189
190
191
192
193 func NewCredentials(opts *CredentialsOptions) *Credentials {
194 creds := &Credentials{
195 TokenProvider: opts.TokenProvider,
196 json: opts.JSON,
197 projectID: opts.ProjectIDProvider,
198 quotaProjectID: opts.QuotaProjectIDProvider,
199 universeDomain: opts.UniverseDomainProvider,
200 }
201
202 return creds
203 }
204
205
206
207 type CachedTokenProviderOptions struct {
208
209
210 DisableAutoRefresh bool
211
212
213 ExpireEarly time.Duration
214 }
215
216 func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
217 if ctpo == nil {
218 return true
219 }
220 return !ctpo.DisableAutoRefresh
221 }
222
223 func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
224 if ctpo == nil {
225 return defaultExpiryDelta
226 }
227 return ctpo.ExpireEarly
228 }
229
230
231
232
233
234 func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
235 if ctp, ok := tp.(*cachedTokenProvider); ok {
236 return ctp
237 }
238 return &cachedTokenProvider{
239 tp: tp,
240 autoRefresh: opts.autoRefresh(),
241 expireEarly: opts.expireEarly(),
242 }
243 }
244
245 type cachedTokenProvider struct {
246 tp TokenProvider
247 autoRefresh bool
248 expireEarly time.Duration
249
250 mu sync.Mutex
251 cachedToken *Token
252 }
253
254 func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
255 c.mu.Lock()
256 defer c.mu.Unlock()
257 if c.cachedToken.IsValid() || !c.autoRefresh {
258 return c.cachedToken, nil
259 }
260 t, err := c.tp.Token(ctx)
261 if err != nil {
262 return nil, err
263 }
264 c.cachedToken = t
265 return t, nil
266 }
267
268
269
270 type Error struct {
271
272
273 Response *http.Response
274
275 Body []byte
276
277 Err error
278
279
280 code string
281
282 description string
283
284 uri string
285 }
286
287 func (e *Error) Error() string {
288 if e.code != "" {
289 s := fmt.Sprintf("auth: %q", e.code)
290 if e.description != "" {
291 s += fmt.Sprintf(" %q", e.description)
292 }
293 if e.uri != "" {
294 s += fmt.Sprintf(" %q", e.uri)
295 }
296 return s
297 }
298 return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
299 }
300
301
302
303 func (e *Error) Temporary() bool {
304 if e.Response == nil {
305 return false
306 }
307 sc := e.Response.StatusCode
308 return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
309 }
310
311 func (e *Error) Unwrap() error {
312 return e.Err
313 }
314
315
316
317 type Style int
318
319 const (
320
321
322 StyleUnknown Style = iota
323
324 StyleInParams
325
326 StyleInHeader
327 )
328
329
330 type Options2LO struct {
331
332
333 Email string
334
335
336
337 PrivateKey []byte
338
339 TokenURL string
340
341
342 PrivateKeyID string
343
344
345 Subject string
346
347 Scopes []string
348
349 Expires time.Duration
350
351 Audience string
352
353 PrivateClaims map[string]interface{}
354
355
356
357 Client *http.Client
358
359
360 UseIDToken bool
361 }
362
363 func (o *Options2LO) client() *http.Client {
364 if o.Client != nil {
365 return o.Client
366 }
367 return internal.CloneDefaultClient()
368 }
369
370 func (o *Options2LO) validate() error {
371 if o == nil {
372 return errors.New("auth: options must be provided")
373 }
374 if o.Email == "" {
375 return errors.New("auth: email must be provided")
376 }
377 if len(o.PrivateKey) == 0 {
378 return errors.New("auth: private key must be provided")
379 }
380 if o.TokenURL == "" {
381 return errors.New("auth: token URL must be provided")
382 }
383 return nil
384 }
385
386
387 func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
388 if err := opts.validate(); err != nil {
389 return nil, err
390 }
391 return tokenProvider2LO{opts: opts, Client: opts.client()}, nil
392 }
393
394 type tokenProvider2LO struct {
395 opts *Options2LO
396 Client *http.Client
397 }
398
399 func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
400 pk, err := internal.ParseKey(tp.opts.PrivateKey)
401 if err != nil {
402 return nil, err
403 }
404 claimSet := &jwt.Claims{
405 Iss: tp.opts.Email,
406 Scope: strings.Join(tp.opts.Scopes, " "),
407 Aud: tp.opts.TokenURL,
408 AdditionalClaims: tp.opts.PrivateClaims,
409 Sub: tp.opts.Subject,
410 }
411 if t := tp.opts.Expires; t > 0 {
412 claimSet.Exp = time.Now().Add(t).Unix()
413 }
414 if aud := tp.opts.Audience; aud != "" {
415 claimSet.Aud = aud
416 }
417 h := *defaultHeader
418 h.KeyID = tp.opts.PrivateKeyID
419 payload, err := jwt.EncodeJWS(&h, claimSet, pk)
420 if err != nil {
421 return nil, err
422 }
423 v := url.Values{}
424 v.Set("grant_type", defaultGrantType)
425 v.Set("assertion", payload)
426 resp, err := tp.Client.PostForm(tp.opts.TokenURL, v)
427 if err != nil {
428 return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
429 }
430 defer resp.Body.Close()
431 body, err := internal.ReadAll(resp.Body)
432 if err != nil {
433 return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
434 }
435 if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
436 return nil, &Error{
437 Response: resp,
438 Body: body,
439 }
440 }
441
442 var tokenRes struct {
443 AccessToken string `json:"access_token"`
444 TokenType string `json:"token_type"`
445 IDToken string `json:"id_token"`
446 ExpiresIn int64 `json:"expires_in"`
447 }
448 if err := json.Unmarshal(body, &tokenRes); err != nil {
449 return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
450 }
451 token := &Token{
452 Value: tokenRes.AccessToken,
453 Type: tokenRes.TokenType,
454 }
455 token.Metadata = make(map[string]interface{})
456 json.Unmarshal(body, &token.Metadata)
457
458 if secs := tokenRes.ExpiresIn; secs > 0 {
459 token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
460 }
461 if v := tokenRes.IDToken; v != "" {
462
463 claimSet, err := jwt.DecodeJWS(v)
464 if err != nil {
465 return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
466 }
467 token.Expiry = time.Unix(claimSet.Exp, 0)
468 }
469 if tp.opts.UseIDToken {
470 if tokenRes.IDToken == "" {
471 return nil, fmt.Errorf("auth: response doesn't have JWT token")
472 }
473 token.Value = tokenRes.IDToken
474 }
475 return token, nil
476 }
477
View as plain text