OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /* | |
6 Package auth defines an opinionated wrapper around OAuth2. | |
7 | |
8 It hides configurability of base oauth2 library and instead makes a predefined | |
9 set of choices regarding where the credentials should be stored and how OAuth2 | |
10 should be used. It makes authentication flows look more uniform across tools | |
11 that use infra.libs.auth and allow credentials reuse across multiple binaries. | |
12 | |
13 Also it knows about various environments Chrome Infra tools are running under | |
14 (GCE, Chrome Infra Golo, GAE, developers' machine) and switches default | |
15 authentication scheme accordingly (e.g. on GCE machine the default is to use | |
16 GCE metadata server). | |
17 | |
18 All tools that use infra.libs.auth share same credentials by default, meaning | |
19 a user needs to authenticate only once to use them all. Credentials are cached | |
20 in ~/.config/chrome_infra/auth/* and reused by all processes running under | |
21 the same user account. | |
22 */ | |
23 package auth | |
24 | |
25 import ( | |
26 "crypto/sha1" | |
27 "encoding/hex" | |
28 "errors" | |
29 "fmt" | |
30 "io/ioutil" | |
31 "net/http" | |
32 "os" | |
33 "os/user" | |
34 "path/filepath" | |
35 "sort" | |
36 "sync" | |
37 | |
38 "golang.org/x/net/context" | |
39 "google.golang.org/cloud/compute/metadata" | |
40 | |
41 "infra/libs/auth/internal" | |
42 "infra/libs/logging" | |
43 ) | |
44 | |
45 var ( | |
46 // ErrLoginRequired is returned by Transport() in case long term credent
ials | |
47 // are not cached and the user must go through interactive login. | |
48 ErrLoginRequired = errors.New("Interactive login is required") | |
49 | |
50 // ErrInsufficientAccess is returned by Login() or Transport() if access
_token | |
51 // can't be minted for given OAuth scopes. For example if GCE instance w
asn't | |
52 // granted access to requested scopes when it was created. | |
53 ErrInsufficientAccess = internal.ErrInsufficientAccess | |
54 ) | |
55 | |
56 // Known Google API OAuth scopes. | |
57 const ( | |
58 OAuthScopeEmail = "https://www.googleapis.com/auth/userinfo.email" | |
59 ) | |
60 | |
61 // Method defines a method to use to obtain OAuth access_token. | |
62 type Method string | |
63 | |
64 // Supported authentication methods. | |
65 const ( | |
66 // AutoSelectMethod can be used to allow the library to pick a method mo
st | |
67 // appropriate for current execution environment. It will search for a p
rivate | |
68 // key for a service account, then (if running on GCE) will try to query
GCE | |
69 // metadata server, and only then pick UserCredentialsMethod that requir
es | |
70 // interaction with a user. | |
71 AutoSelectMethod Method = "" | |
72 | |
73 // UserCredentialsMethod is used for interactive OAuth 3-legged login fl
ow. | |
74 UserCredentialsMethod Method = "UserCredentialsMethod" | |
75 | |
76 // ServiceAccountMethod is used to authenticate as a service account usi
ng | |
77 // a private key. | |
78 ServiceAccountMethod Method = "ServiceAccountMethod" | |
79 | |
80 // GCEMetadataMethod is used on Compute Engine to use tokens provided by | |
81 // Metadata server. See https://cloud.google.com/compute/docs/authentica
tion | |
82 GCEMetadataMethod Method = "GCEMetadataMethod" | |
83 ) | |
84 | |
85 // LoginMode is used as enum in AuthenticatedClient function. | |
86 type LoginMode string | |
87 | |
88 const ( | |
89 // InteractiveLogin is passed to AuthenticatedClient to forcefully rerun
full | |
90 // login flow and cache resulting tokens. Used by 'login' CLI command. | |
91 InteractiveLogin LoginMode = "InteractiveLogin" | |
92 | |
93 // SilentLogin is passed to AuthenticatedClient if authentication must b
e used | |
94 // and it is NOT OK to run interactive login flow to get the tokens. The
call | |
95 // will fail with ErrLoginRequired error if there's no cached tokens. Sh
ould | |
96 // normally be used by all CLI tools that need to use authentication. | |
97 SilentLogin LoginMode = "SilentLogin" | |
98 | |
99 // OptionalLogin is passed to AuthenticatedClient if it is OK not to use | |
100 // authentication if there are no cached credentials. Interactive login
will | |
101 // never be called, default unauthenticated client will be returned inst
ead. | |
102 // Should be used by CLI tools where authentication is optional. | |
103 OptionalLogin LoginMode = "OptionalLogin" | |
104 ) | |
105 | |
106 // Options are used by NewAuthenticator call. All fields are optional and have | |
107 // sane default values. | |
108 type Options struct { | |
109 // Method defaults to AutoSelectMethod. | |
110 Method Method | |
111 // Scopes is a list of OAuth scopes to request, defaults to [OAuthScopeE
mail]. | |
112 Scopes []string | |
113 | |
114 // ClientID is OAuth client_id to use with UserCredentialsMethod. | |
115 // Default: provided by DefaultClient(). | |
116 ClientID string | |
117 // ClientID is OAuth client_secret to use with UserCredentialsMethod. | |
118 // Default: provided by DefaultClient(). | |
119 ClientSecret string | |
120 | |
121 // ServiceAccountJSONPath is a path to a JSON blob with a private key to
use | |
122 // with ServiceAccountMethod. See the "Credentials" page under "APIs & A
uth" | |
123 // for your project at Cloud Console. | |
124 // Default: ~/.config/chrome_infra/auth/service_account.json. | |
125 ServiceAccountJSONPath string | |
126 | |
127 // GCEAccountName is an account name to query to fetch token for from me
tadata | |
128 // server when GCEMetadataMethod is used. If given account wasn't grante
d | |
129 // required set of scopes during instance creation time, Transport() cal
l | |
130 // fails with ErrInsufficientAccess. | |
131 // Default: "default" account. | |
132 GCEAccountName string | |
133 | |
134 // Context carries the underlying HTTP transport to use. If context is n
ot | |
135 // provided or doesn't contain the transport, http.DefaultTransport will
be | |
136 // used. Context will also be used to grab a logger if passed Logger is
nil. | |
137 Context context.Context | |
138 | |
139 // Logger is used to write log messages. If nil, extract it from the con
text. | |
140 Logger logging.Logger | |
141 } | |
142 | |
143 // Authenticator is a factory for http.RoundTripper objects that know how to use | |
144 // cached OAuth credentials. Authenticator also knows how to run interactive | |
145 // login flow, if required. | |
146 type Authenticator interface { | |
147 // Transport returns http.RoundTripper that adds authentication details
to | |
148 // each request. An interactive authentication flow (if required) must b
e | |
149 // complete before making a transport, otherwise ErrLoginRequired is ret
urned. | |
150 // Returned transport object can be safely reused across many http.Clien
t's. | |
151 Transport() (http.RoundTripper, error) | |
152 | |
153 // Login perform an interaction with the user to get a long term refresh
token | |
154 // and cache it. Blocks for user input, can use stdin. Returns ErrNoTerm
inal | |
155 // if interaction with a user is required, but the process is not runnin
g | |
156 // under a terminal. It overwrites currently cached credentials, if any. | |
157 Login() error | |
158 | |
159 // PurgeCredentialsCache removes cached tokens. | |
160 PurgeCredentialsCache() error | |
161 } | |
162 | |
163 // NewAuthenticator returns a new instance of Authenticator given its options. | |
164 func NewAuthenticator(opts Options) Authenticator { | |
165 // Add default scope, sort scopes. | |
166 if len(opts.Scopes) == 0 { | |
167 opts.Scopes = []string{OAuthScopeEmail} | |
168 } | |
169 tmp := make([]string, len(opts.Scopes)) | |
170 copy(tmp, opts.Scopes) | |
171 sort.Strings(tmp) | |
172 opts.Scopes = tmp | |
173 | |
174 // Fill in blanks with default values. | |
175 if opts.ClientID == "" || opts.ClientSecret == "" { | |
176 opts.ClientID, opts.ClientSecret = DefaultClient() | |
177 } | |
178 if opts.ServiceAccountJSONPath == "" { | |
179 opts.ServiceAccountJSONPath = filepath.Join(SecretsDir(), "servi
ce_account.json") | |
180 } | |
181 if opts.GCEAccountName == "" { | |
182 opts.GCEAccountName = "default" | |
183 } | |
184 if opts.Context == nil { | |
185 opts.Context = context.Background() | |
186 } | |
187 if opts.Logger == nil { | |
188 opts.Logger = logging.Get(opts.Context) | |
189 } | |
190 | |
191 // See ensureInitialized for the rest of the initialization. | |
192 auth := &authenticatorImpl{opts: &opts, log: opts.Logger} | |
193 auth.transport = &authTransport{ | |
194 parent: auth, | |
195 base: internal.TransportFromContext(opts.Context), | |
196 log: opts.Logger, | |
197 } | |
198 return auth | |
199 } | |
200 | |
201 // AuthenticatedClient performs login (if requested) and returns http.Client. | |
202 // See documentation for 'mode' for more details. | |
203 func AuthenticatedClient(mode LoginMode, auth Authenticator) (*http.Client, erro
r) { | |
204 if mode == InteractiveLogin { | |
205 if err := auth.PurgeCredentialsCache(); err != nil { | |
206 return nil, err | |
207 } | |
208 } | |
209 transport, err := auth.Transport() | |
210 if err == nil { | |
211 return &http.Client{Transport: transport}, nil | |
212 } | |
213 if err != ErrLoginRequired || mode == SilentLogin { | |
214 return nil, err | |
215 } | |
216 if mode == OptionalLogin { | |
217 return http.DefaultClient, nil | |
218 } | |
219 if mode != InteractiveLogin { | |
220 return nil, fmt.Errorf("Invalid mode argument: %s", mode) | |
221 } | |
222 if err = auth.Login(); err != nil { | |
223 return nil, err | |
224 } | |
225 if transport, err = auth.Transport(); err != nil { | |
226 return nil, err | |
227 } | |
228 return &http.Client{Transport: transport}, nil | |
229 } | |
230 | |
231 //////////////////////////////////////////////////////////////////////////////// | |
232 // Authenticator implementation. | |
233 | |
234 type authenticatorImpl struct { | |
235 // Immutable members. | |
236 opts *Options | |
237 transport http.RoundTripper | |
238 log logging.Logger | |
239 | |
240 // Mutable members. | |
241 lock sync.Mutex | |
242 cache *tokenCache | |
243 provider internal.TokenProvider | |
244 err error | |
245 token internal.Token | |
246 } | |
247 | |
248 func (a *authenticatorImpl) Transport() (http.RoundTripper, error) { | |
249 a.lock.Lock() | |
250 defer a.lock.Unlock() | |
251 | |
252 err := a.ensureInitialized() | |
253 if err != nil { | |
254 return nil, err | |
255 } | |
256 | |
257 // No cached token and token provider requires interaction with a user:
need | |
258 // to login. Only non-interactive token providers are allowed to mint to
kens | |
259 // on the fly, see refreshToken. | |
260 if a.token == nil && a.provider.RequiresInteraction() { | |
261 return nil, ErrLoginRequired | |
262 } | |
263 return a.transport, nil | |
264 } | |
265 | |
266 func (a *authenticatorImpl) Login() error { | |
267 a.lock.Lock() | |
268 defer a.lock.Unlock() | |
269 | |
270 err := a.ensureInitialized() | |
271 if err != nil { | |
272 return err | |
273 } | |
274 if !a.provider.RequiresInteraction() { | |
275 return nil | |
276 } | |
277 | |
278 // Create initial token. This may require interaction with a user. | |
279 a.token, err = a.provider.MintToken() | |
280 if err != nil { | |
281 return err | |
282 } | |
283 | |
284 // Store the initial token in the cache. Don't abort if it fails, the to
ken | |
285 // is still usable from the memory. | |
286 if err = a.cacheToken(a.token); err != nil { | |
287 a.log.Warningf("auth: failed to write token to cache: %v", err) | |
288 } | |
289 | |
290 return nil | |
291 } | |
292 | |
293 func (a *authenticatorImpl) PurgeCredentialsCache() error { | |
294 a.lock.Lock() | |
295 defer a.lock.Unlock() | |
296 | |
297 if err := a.ensureInitialized(); err != nil { | |
298 return err | |
299 } | |
300 if err := a.cache.clear(); err != nil { | |
301 return err | |
302 } | |
303 a.token = nil | |
304 return nil | |
305 } | |
306 | |
307 //////////////////////////////////////////////////////////////////////////////// | |
308 // Authenticator private methods. | |
309 | |
310 // ensureInitialized is supposed to be called under the lock. | |
311 func (a *authenticatorImpl) ensureInitialized() error { | |
312 if a.err != nil || a.provider != nil { | |
313 return a.err | |
314 } | |
315 | |
316 // selectDefaultMethod may do heavy calls, call it lazily here rather th
an in | |
317 // NewAuthenticator. | |
318 if a.opts.Method == AutoSelectMethod { | |
319 a.opts.Method = selectDefaultMethod(a.opts) | |
320 } | |
321 a.log.Infof("auth: using %s", a.opts.Method) | |
322 a.provider, a.err = makeTokenProvider(a.opts) | |
323 if a.err != nil { | |
324 return a.err | |
325 } | |
326 | |
327 // Setup the cache only when Method is known, cache filename depends on
it. | |
328 a.cache = &tokenCache{ | |
329 path: filepath.Join(SecretsDir(), cacheFileName(a.opts)+".tok"), | |
330 log: a.log, | |
331 } | |
332 | |
333 // Broken token cache is not a fatal error. So just log it and forget, a
new | |
334 // token will be minted. | |
335 var err error | |
336 a.token, err = a.readTokenCache() | |
337 if err != nil { | |
338 a.log.Warningf("auth: failed to read token from cache: %v", err) | |
339 } | |
340 return nil | |
341 } | |
342 | |
343 // readTokenCache may be called with a.lock held or not held. It works either wa
y. | |
344 func (a *authenticatorImpl) readTokenCache() (internal.Token, error) { | |
345 // 'read' returns (nil, nil) if cache is empty. | |
346 buf, err := a.cache.read() | |
347 if err != nil || buf == nil { | |
348 return nil, err | |
349 } | |
350 token, err := a.provider.UnmarshalToken(buf) | |
351 if err != nil { | |
352 return nil, err | |
353 } | |
354 return token, nil | |
355 } | |
356 | |
357 // cacheToken may be called with a.lock held or not held. It works either way. | |
358 func (a *authenticatorImpl) cacheToken(tok internal.Token) error { | |
359 buf, err := a.provider.MarshalToken(tok) | |
360 if err != nil { | |
361 return err | |
362 } | |
363 return a.cache.write(buf) | |
364 } | |
365 | |
366 // currentToken lock a.lock inside. It MUST NOT be called when a.lock is held. | |
367 func (a *authenticatorImpl) currentToken() internal.Token { | |
368 // TODO(vadimsh): Test with go test -race. The lock may be unnecessary. | |
369 a.lock.Lock() | |
370 defer a.lock.Unlock() | |
371 return a.token | |
372 } | |
373 | |
374 // refreshToken compares current token to 'prev' and launches token refresh | |
375 // procedure if they still match. Returns a refreshed token (if a refresh | |
376 // procedure happened) or the current token (i.e. if it's different from prev). | |
377 // Acts as "Compare-And-Swap" where "Swap" is a token refresh procedure. | |
378 func (a *authenticatorImpl) refreshToken(prev internal.Token) (internal.Token, e
rror) { | |
379 // Refresh the token under the lock. | |
380 tok, cache, err := func() (internal.Token, bool, error) { | |
381 a.lock.Lock() | |
382 defer a.lock.Unlock() | |
383 | |
384 // Some other goroutine already updated the token, just return t
he token. | |
385 if a.token != nil && !a.token.Equals(prev) { | |
386 return a.token, false, nil | |
387 } | |
388 | |
389 // Rescan the cache. Maybe some other process updated the token. | |
390 cached, err := a.readTokenCache() | |
391 if err == nil && cached != nil && !cached.Equals(prev) && !cache
d.Expired() { | |
392 a.log.Infof("auth: some other process put refreshed toke
n in the cache") | |
393 a.token = cached | |
394 return a.token, false, nil | |
395 } | |
396 | |
397 // Mint a new token or refresh the existing one. | |
398 if a.token == nil { | |
399 // Can't do user interaction outside of Login. | |
400 if a.provider.RequiresInteraction() { | |
401 return nil, false, ErrLoginRequired | |
402 } | |
403 a.log.Infof("auth: minting a new token") | |
404 a.token, err = a.provider.MintToken() | |
405 if err != nil { | |
406 a.log.Warningf("auth: failed to mint a token: %v
", err) | |
407 return nil, false, err | |
408 } | |
409 } else { | |
410 a.log.Infof("auth: refreshing the token") | |
411 a.token, err = a.provider.RefreshToken(a.token) | |
412 if err != nil { | |
413 a.log.Warningf("auth: failed to refresh the toke
n: %v", err) | |
414 return nil, false, err | |
415 } | |
416 } | |
417 return a.token, true, nil | |
418 }() | |
419 | |
420 if err != nil { | |
421 return nil, err | |
422 } | |
423 | |
424 // Store the new token in the cache outside the lock, no need for caller
s to | |
425 // wait for this. Do not die if failed, token is still usable from the m
emory. | |
426 if cache { | |
427 if err = a.cacheToken(tok); err != nil { | |
428 a.log.Warningf("auth: failed to write refreshed token to
the cache: %v", err) | |
429 } | |
430 } | |
431 return tok, nil | |
432 } | |
433 | |
434 //////////////////////////////////////////////////////////////////////////////// | |
435 // authTransport implementation. | |
436 | |
437 // TODO(vadimsh): Support CancelRequest if underlying transport supports it. | |
438 // It's tricky. http.Client uses type cast to figure out whether transport | |
439 // supports request cancellation or not. So new authTransportWithCancelation | |
440 // should be used when parent transport provides CancelRequest. Also | |
441 // authTransport should keep a mapping between original http.Request objects | |
442 // and ones with access tokens attached (to know what to pass to | |
443 // parent transport CancelRequest). | |
444 | |
445 type authTransport struct { | |
446 parent *authenticatorImpl | |
447 base http.RoundTripper | |
448 log logging.Logger | |
449 } | |
450 | |
451 // RoundTrip appends authorization details to the request. | |
452 func (t *authTransport) RoundTrip(req *http.Request) (resp *http.Response, err e
rror) { | |
453 tok := t.parent.currentToken() | |
454 if tok == nil || tok.Expired() { | |
455 tok, err = t.parent.refreshToken(tok) | |
456 if err != nil { | |
457 return | |
458 } | |
459 if tok == nil || tok.Expired() { | |
460 err = fmt.Errorf("auth: failed to refresh the token") | |
461 return | |
462 } | |
463 } | |
464 clone := *req | |
465 clone.Header = make(http.Header) | |
466 for k, v := range req.Header { | |
467 clone.Header[k] = v | |
468 } | |
469 for k, v := range tok.RequestHeaders() { | |
470 clone.Header.Set(k, v) | |
471 } | |
472 return t.base.RoundTrip(&clone) | |
473 } | |
474 | |
475 //////////////////////////////////////////////////////////////////////////////// | |
476 // tokenCache implementation. | |
477 | |
478 type tokenCache struct { | |
479 path string | |
480 log logging.Logger | |
481 lock sync.Mutex | |
482 } | |
483 | |
484 func (c *tokenCache) read() (buf []byte, err error) { | |
485 c.lock.Lock() | |
486 defer c.lock.Unlock() | |
487 c.log.Infof("auth: reading token from %s", c.path) | |
488 buf, err = ioutil.ReadFile(c.path) | |
489 if err != nil && os.IsNotExist(err) { | |
490 err = nil | |
491 } | |
492 return | |
493 } | |
494 | |
495 func (c *tokenCache) write(buf []byte) error { | |
496 c.lock.Lock() | |
497 defer c.lock.Unlock() | |
498 c.log.Infof("auth: writing token to %s", c.path) | |
499 err := os.MkdirAll(filepath.Dir(c.path), 0700) | |
500 if err != nil { | |
501 return err | |
502 } | |
503 // TODO(vadimsh): Make it atomic across multiple processes. | |
504 return ioutil.WriteFile(c.path, buf, 0600) | |
505 } | |
506 | |
507 func (c *tokenCache) clear() error { | |
508 c.lock.Lock() | |
509 defer c.lock.Unlock() | |
510 err := os.Remove(c.path) | |
511 if err != nil && !os.IsNotExist(err) { | |
512 return err | |
513 } | |
514 return nil | |
515 } | |
516 | |
517 //////////////////////////////////////////////////////////////////////////////// | |
518 // Utility functions. | |
519 | |
520 func cacheFileName(opts *Options) string { | |
521 // Construct a name of cache file from data that identifies requested | |
522 // credential, to allow multiple differently configured instances of | |
523 // Authenticator to coexist. | |
524 sum := sha1.New() | |
525 sum.Write([]byte(opts.Method)) | |
526 sum.Write([]byte{0}) | |
527 sum.Write([]byte(opts.ClientID)) | |
528 sum.Write([]byte{0}) | |
529 sum.Write([]byte(opts.ClientSecret)) | |
530 sum.Write([]byte{0}) | |
531 for _, scope := range opts.Scopes { | |
532 sum.Write([]byte(scope)) | |
533 sum.Write([]byte{0}) | |
534 } | |
535 sum.Write([]byte(opts.GCEAccountName)) | |
536 return hex.EncodeToString(sum.Sum(nil))[:16] | |
537 } | |
538 | |
539 // selectDefaultMethod is mocked in tests. | |
540 var selectDefaultMethod = func(opts *Options) Method { | |
541 if opts.ServiceAccountJSONPath != "" { | |
542 info, _ := os.Stat(opts.ServiceAccountJSONPath) | |
543 if info != nil && info.Mode().IsRegular() { | |
544 return ServiceAccountMethod | |
545 } | |
546 } | |
547 if metadata.OnGCE() { | |
548 return GCEMetadataMethod | |
549 } | |
550 return UserCredentialsMethod | |
551 } | |
552 | |
553 // secretsDir is mocked in tests. Called by publicly visible SecretsDir(). | |
554 var secretsDir = func() string { | |
555 usr, err := user.Current() | |
556 if err != nil { | |
557 panic(err.Error()) | |
558 } | |
559 // TODO(vadimsh): On Windows use SHGetFolderPath with CSIDL_LOCAL_APPDAT
A to | |
560 // locate a directory to store app files. | |
561 return filepath.Join(usr.HomeDir, ".config", "chrome_infra", "auth") | |
562 } | |
563 | |
564 // makeTokenProvider is mocked in tests. Called by ensureInitialized. | |
565 var makeTokenProvider = func(opts *Options) (internal.TokenProvider, error) { | |
566 switch opts.Method { | |
567 case UserCredentialsMethod: | |
568 return internal.NewUserAuthTokenProvider( | |
569 opts.Context, | |
570 opts.ClientID, | |
571 opts.ClientSecret, | |
572 opts.Scopes) | |
573 case ServiceAccountMethod: | |
574 return internal.NewServiceAccountTokenProvider( | |
575 opts.Context, | |
576 opts.ServiceAccountJSONPath, | |
577 opts.Scopes) | |
578 case GCEMetadataMethod: | |
579 return internal.NewGCETokenProvider( | |
580 opts.GCEAccountName, | |
581 opts.Scopes) | |
582 default: | |
583 return nil, fmt.Errorf("Unrecognized authentication method: %s",
opts.Method) | |
584 } | |
585 } | |
586 | |
587 // DefaultClient returns OAuth client_id and client_secret to use for 3 legged | |
588 // OAuth flow. Note that client_secret is not really a secret since it's | |
589 // hardcoded into the source code (and binaries). It's totally fine, as long | |
590 // as it's callback URI is configured to be 'localhost'. If someone decides to | |
591 // reuse such client_secret they have to run something on user's local machine | |
592 // to get the refresh_token. | |
593 func DefaultClient() (clientID string, clientSecret string) { | |
594 clientID = "446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936.apps.googleuse
rcontent.com" | |
595 clientSecret = "uBfbay2KCy9t4QveJ-dOqHtp" | |
596 return | |
597 } | |
598 | |
599 // SecretsDir returns an absolute path to a directory to keep secret files in. | |
600 func SecretsDir() string { | |
601 return secretsDir() | |
602 } | |
OLD | NEW |