Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(286)

Side by Side Diff: go/src/infra/libs/auth/auth.go

Issue 1153883002: go: infra/libs/* now live in luci-go. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: move the rest too Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « go/src/infra/gae/libs/meta/eg.go ('k') | go/src/infra/libs/auth/auth.infra_testing » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « go/src/infra/gae/libs/meta/eg.go ('k') | go/src/infra/libs/auth/auth.infra_testing » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698