Index: go/src/infra/libs/auth/auth.go |
diff --git a/go/src/infra/libs/auth/auth.go b/go/src/infra/libs/auth/auth.go |
deleted file mode 100644 |
index 9082eb14e69c1feba6fbe714127ad01d439d6567..0000000000000000000000000000000000000000 |
--- a/go/src/infra/libs/auth/auth.go |
+++ /dev/null |
@@ -1,602 +0,0 @@ |
-// Copyright 2014 The Chromium Authors. All rights reserved. |
-// Use of this source code is governed by a BSD-style license that can be |
-// found in the LICENSE file. |
- |
-/* |
-Package auth defines an opinionated wrapper around OAuth2. |
- |
-It hides configurability of base oauth2 library and instead makes a predefined |
-set of choices regarding where the credentials should be stored and how OAuth2 |
-should be used. It makes authentication flows look more uniform across tools |
-that use infra.libs.auth and allow credentials reuse across multiple binaries. |
- |
-Also it knows about various environments Chrome Infra tools are running under |
-(GCE, Chrome Infra Golo, GAE, developers' machine) and switches default |
-authentication scheme accordingly (e.g. on GCE machine the default is to use |
-GCE metadata server). |
- |
-All tools that use infra.libs.auth share same credentials by default, meaning |
-a user needs to authenticate only once to use them all. Credentials are cached |
-in ~/.config/chrome_infra/auth/* and reused by all processes running under |
-the same user account. |
-*/ |
-package auth |
- |
-import ( |
- "crypto/sha1" |
- "encoding/hex" |
- "errors" |
- "fmt" |
- "io/ioutil" |
- "net/http" |
- "os" |
- "os/user" |
- "path/filepath" |
- "sort" |
- "sync" |
- |
- "golang.org/x/net/context" |
- "google.golang.org/cloud/compute/metadata" |
- |
- "infra/libs/auth/internal" |
- "infra/libs/logging" |
-) |
- |
-var ( |
- // ErrLoginRequired is returned by Transport() in case long term credentials |
- // are not cached and the user must go through interactive login. |
- ErrLoginRequired = errors.New("Interactive login is required") |
- |
- // ErrInsufficientAccess is returned by Login() or Transport() if access_token |
- // can't be minted for given OAuth scopes. For example if GCE instance wasn't |
- // granted access to requested scopes when it was created. |
- ErrInsufficientAccess = internal.ErrInsufficientAccess |
-) |
- |
-// Known Google API OAuth scopes. |
-const ( |
- OAuthScopeEmail = "https://www.googleapis.com/auth/userinfo.email" |
-) |
- |
-// Method defines a method to use to obtain OAuth access_token. |
-type Method string |
- |
-// Supported authentication methods. |
-const ( |
- // AutoSelectMethod can be used to allow the library to pick a method most |
- // appropriate for current execution environment. It will search for a private |
- // key for a service account, then (if running on GCE) will try to query GCE |
- // metadata server, and only then pick UserCredentialsMethod that requires |
- // interaction with a user. |
- AutoSelectMethod Method = "" |
- |
- // UserCredentialsMethod is used for interactive OAuth 3-legged login flow. |
- UserCredentialsMethod Method = "UserCredentialsMethod" |
- |
- // ServiceAccountMethod is used to authenticate as a service account using |
- // a private key. |
- ServiceAccountMethod Method = "ServiceAccountMethod" |
- |
- // GCEMetadataMethod is used on Compute Engine to use tokens provided by |
- // Metadata server. See https://cloud.google.com/compute/docs/authentication |
- GCEMetadataMethod Method = "GCEMetadataMethod" |
-) |
- |
-// LoginMode is used as enum in AuthenticatedClient function. |
-type LoginMode string |
- |
-const ( |
- // InteractiveLogin is passed to AuthenticatedClient to forcefully rerun full |
- // login flow and cache resulting tokens. Used by 'login' CLI command. |
- InteractiveLogin LoginMode = "InteractiveLogin" |
- |
- // SilentLogin is passed to AuthenticatedClient if authentication must be used |
- // and it is NOT OK to run interactive login flow to get the tokens. The call |
- // will fail with ErrLoginRequired error if there's no cached tokens. Should |
- // normally be used by all CLI tools that need to use authentication. |
- SilentLogin LoginMode = "SilentLogin" |
- |
- // OptionalLogin is passed to AuthenticatedClient if it is OK not to use |
- // authentication if there are no cached credentials. Interactive login will |
- // never be called, default unauthenticated client will be returned instead. |
- // Should be used by CLI tools where authentication is optional. |
- OptionalLogin LoginMode = "OptionalLogin" |
-) |
- |
-// Options are used by NewAuthenticator call. All fields are optional and have |
-// sane default values. |
-type Options struct { |
- // Method defaults to AutoSelectMethod. |
- Method Method |
- // Scopes is a list of OAuth scopes to request, defaults to [OAuthScopeEmail]. |
- Scopes []string |
- |
- // ClientID is OAuth client_id to use with UserCredentialsMethod. |
- // Default: provided by DefaultClient(). |
- ClientID string |
- // ClientID is OAuth client_secret to use with UserCredentialsMethod. |
- // Default: provided by DefaultClient(). |
- ClientSecret string |
- |
- // ServiceAccountJSONPath is a path to a JSON blob with a private key to use |
- // with ServiceAccountMethod. See the "Credentials" page under "APIs & Auth" |
- // for your project at Cloud Console. |
- // Default: ~/.config/chrome_infra/auth/service_account.json. |
- ServiceAccountJSONPath string |
- |
- // GCEAccountName is an account name to query to fetch token for from metadata |
- // server when GCEMetadataMethod is used. If given account wasn't granted |
- // required set of scopes during instance creation time, Transport() call |
- // fails with ErrInsufficientAccess. |
- // Default: "default" account. |
- GCEAccountName string |
- |
- // Context carries the underlying HTTP transport to use. If context is not |
- // provided or doesn't contain the transport, http.DefaultTransport will be |
- // used. Context will also be used to grab a logger if passed Logger is nil. |
- Context context.Context |
- |
- // Logger is used to write log messages. If nil, extract it from the context. |
- Logger logging.Logger |
-} |
- |
-// Authenticator is a factory for http.RoundTripper objects that know how to use |
-// cached OAuth credentials. Authenticator also knows how to run interactive |
-// login flow, if required. |
-type Authenticator interface { |
- // Transport returns http.RoundTripper that adds authentication details to |
- // each request. An interactive authentication flow (if required) must be |
- // complete before making a transport, otherwise ErrLoginRequired is returned. |
- // Returned transport object can be safely reused across many http.Client's. |
- Transport() (http.RoundTripper, error) |
- |
- // Login perform an interaction with the user to get a long term refresh token |
- // and cache it. Blocks for user input, can use stdin. Returns ErrNoTerminal |
- // if interaction with a user is required, but the process is not running |
- // under a terminal. It overwrites currently cached credentials, if any. |
- Login() error |
- |
- // PurgeCredentialsCache removes cached tokens. |
- PurgeCredentialsCache() error |
-} |
- |
-// NewAuthenticator returns a new instance of Authenticator given its options. |
-func NewAuthenticator(opts Options) Authenticator { |
- // Add default scope, sort scopes. |
- if len(opts.Scopes) == 0 { |
- opts.Scopes = []string{OAuthScopeEmail} |
- } |
- tmp := make([]string, len(opts.Scopes)) |
- copy(tmp, opts.Scopes) |
- sort.Strings(tmp) |
- opts.Scopes = tmp |
- |
- // Fill in blanks with default values. |
- if opts.ClientID == "" || opts.ClientSecret == "" { |
- opts.ClientID, opts.ClientSecret = DefaultClient() |
- } |
- if opts.ServiceAccountJSONPath == "" { |
- opts.ServiceAccountJSONPath = filepath.Join(SecretsDir(), "service_account.json") |
- } |
- if opts.GCEAccountName == "" { |
- opts.GCEAccountName = "default" |
- } |
- if opts.Context == nil { |
- opts.Context = context.Background() |
- } |
- if opts.Logger == nil { |
- opts.Logger = logging.Get(opts.Context) |
- } |
- |
- // See ensureInitialized for the rest of the initialization. |
- auth := &authenticatorImpl{opts: &opts, log: opts.Logger} |
- auth.transport = &authTransport{ |
- parent: auth, |
- base: internal.TransportFromContext(opts.Context), |
- log: opts.Logger, |
- } |
- return auth |
-} |
- |
-// AuthenticatedClient performs login (if requested) and returns http.Client. |
-// See documentation for 'mode' for more details. |
-func AuthenticatedClient(mode LoginMode, auth Authenticator) (*http.Client, error) { |
- if mode == InteractiveLogin { |
- if err := auth.PurgeCredentialsCache(); err != nil { |
- return nil, err |
- } |
- } |
- transport, err := auth.Transport() |
- if err == nil { |
- return &http.Client{Transport: transport}, nil |
- } |
- if err != ErrLoginRequired || mode == SilentLogin { |
- return nil, err |
- } |
- if mode == OptionalLogin { |
- return http.DefaultClient, nil |
- } |
- if mode != InteractiveLogin { |
- return nil, fmt.Errorf("Invalid mode argument: %s", mode) |
- } |
- if err = auth.Login(); err != nil { |
- return nil, err |
- } |
- if transport, err = auth.Transport(); err != nil { |
- return nil, err |
- } |
- return &http.Client{Transport: transport}, nil |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// Authenticator implementation. |
- |
-type authenticatorImpl struct { |
- // Immutable members. |
- opts *Options |
- transport http.RoundTripper |
- log logging.Logger |
- |
- // Mutable members. |
- lock sync.Mutex |
- cache *tokenCache |
- provider internal.TokenProvider |
- err error |
- token internal.Token |
-} |
- |
-func (a *authenticatorImpl) Transport() (http.RoundTripper, error) { |
- a.lock.Lock() |
- defer a.lock.Unlock() |
- |
- err := a.ensureInitialized() |
- if err != nil { |
- return nil, err |
- } |
- |
- // No cached token and token provider requires interaction with a user: need |
- // to login. Only non-interactive token providers are allowed to mint tokens |
- // on the fly, see refreshToken. |
- if a.token == nil && a.provider.RequiresInteraction() { |
- return nil, ErrLoginRequired |
- } |
- return a.transport, nil |
-} |
- |
-func (a *authenticatorImpl) Login() error { |
- a.lock.Lock() |
- defer a.lock.Unlock() |
- |
- err := a.ensureInitialized() |
- if err != nil { |
- return err |
- } |
- if !a.provider.RequiresInteraction() { |
- return nil |
- } |
- |
- // Create initial token. This may require interaction with a user. |
- a.token, err = a.provider.MintToken() |
- if err != nil { |
- return err |
- } |
- |
- // Store the initial token in the cache. Don't abort if it fails, the token |
- // is still usable from the memory. |
- if err = a.cacheToken(a.token); err != nil { |
- a.log.Warningf("auth: failed to write token to cache: %v", err) |
- } |
- |
- return nil |
-} |
- |
-func (a *authenticatorImpl) PurgeCredentialsCache() error { |
- a.lock.Lock() |
- defer a.lock.Unlock() |
- |
- if err := a.ensureInitialized(); err != nil { |
- return err |
- } |
- if err := a.cache.clear(); err != nil { |
- return err |
- } |
- a.token = nil |
- return nil |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// Authenticator private methods. |
- |
-// ensureInitialized is supposed to be called under the lock. |
-func (a *authenticatorImpl) ensureInitialized() error { |
- if a.err != nil || a.provider != nil { |
- return a.err |
- } |
- |
- // selectDefaultMethod may do heavy calls, call it lazily here rather than in |
- // NewAuthenticator. |
- if a.opts.Method == AutoSelectMethod { |
- a.opts.Method = selectDefaultMethod(a.opts) |
- } |
- a.log.Infof("auth: using %s", a.opts.Method) |
- a.provider, a.err = makeTokenProvider(a.opts) |
- if a.err != nil { |
- return a.err |
- } |
- |
- // Setup the cache only when Method is known, cache filename depends on it. |
- a.cache = &tokenCache{ |
- path: filepath.Join(SecretsDir(), cacheFileName(a.opts)+".tok"), |
- log: a.log, |
- } |
- |
- // Broken token cache is not a fatal error. So just log it and forget, a new |
- // token will be minted. |
- var err error |
- a.token, err = a.readTokenCache() |
- if err != nil { |
- a.log.Warningf("auth: failed to read token from cache: %v", err) |
- } |
- return nil |
-} |
- |
-// readTokenCache may be called with a.lock held or not held. It works either way. |
-func (a *authenticatorImpl) readTokenCache() (internal.Token, error) { |
- // 'read' returns (nil, nil) if cache is empty. |
- buf, err := a.cache.read() |
- if err != nil || buf == nil { |
- return nil, err |
- } |
- token, err := a.provider.UnmarshalToken(buf) |
- if err != nil { |
- return nil, err |
- } |
- return token, nil |
-} |
- |
-// cacheToken may be called with a.lock held or not held. It works either way. |
-func (a *authenticatorImpl) cacheToken(tok internal.Token) error { |
- buf, err := a.provider.MarshalToken(tok) |
- if err != nil { |
- return err |
- } |
- return a.cache.write(buf) |
-} |
- |
-// currentToken lock a.lock inside. It MUST NOT be called when a.lock is held. |
-func (a *authenticatorImpl) currentToken() internal.Token { |
- // TODO(vadimsh): Test with go test -race. The lock may be unnecessary. |
- a.lock.Lock() |
- defer a.lock.Unlock() |
- return a.token |
-} |
- |
-// refreshToken compares current token to 'prev' and launches token refresh |
-// procedure if they still match. Returns a refreshed token (if a refresh |
-// procedure happened) or the current token (i.e. if it's different from prev). |
-// Acts as "Compare-And-Swap" where "Swap" is a token refresh procedure. |
-func (a *authenticatorImpl) refreshToken(prev internal.Token) (internal.Token, error) { |
- // Refresh the token under the lock. |
- tok, cache, err := func() (internal.Token, bool, error) { |
- a.lock.Lock() |
- defer a.lock.Unlock() |
- |
- // Some other goroutine already updated the token, just return the token. |
- if a.token != nil && !a.token.Equals(prev) { |
- return a.token, false, nil |
- } |
- |
- // Rescan the cache. Maybe some other process updated the token. |
- cached, err := a.readTokenCache() |
- if err == nil && cached != nil && !cached.Equals(prev) && !cached.Expired() { |
- a.log.Infof("auth: some other process put refreshed token in the cache") |
- a.token = cached |
- return a.token, false, nil |
- } |
- |
- // Mint a new token or refresh the existing one. |
- if a.token == nil { |
- // Can't do user interaction outside of Login. |
- if a.provider.RequiresInteraction() { |
- return nil, false, ErrLoginRequired |
- } |
- a.log.Infof("auth: minting a new token") |
- a.token, err = a.provider.MintToken() |
- if err != nil { |
- a.log.Warningf("auth: failed to mint a token: %v", err) |
- return nil, false, err |
- } |
- } else { |
- a.log.Infof("auth: refreshing the token") |
- a.token, err = a.provider.RefreshToken(a.token) |
- if err != nil { |
- a.log.Warningf("auth: failed to refresh the token: %v", err) |
- return nil, false, err |
- } |
- } |
- return a.token, true, nil |
- }() |
- |
- if err != nil { |
- return nil, err |
- } |
- |
- // Store the new token in the cache outside the lock, no need for callers to |
- // wait for this. Do not die if failed, token is still usable from the memory. |
- if cache { |
- if err = a.cacheToken(tok); err != nil { |
- a.log.Warningf("auth: failed to write refreshed token to the cache: %v", err) |
- } |
- } |
- return tok, nil |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// authTransport implementation. |
- |
-// TODO(vadimsh): Support CancelRequest if underlying transport supports it. |
-// It's tricky. http.Client uses type cast to figure out whether transport |
-// supports request cancellation or not. So new authTransportWithCancelation |
-// should be used when parent transport provides CancelRequest. Also |
-// authTransport should keep a mapping between original http.Request objects |
-// and ones with access tokens attached (to know what to pass to |
-// parent transport CancelRequest). |
- |
-type authTransport struct { |
- parent *authenticatorImpl |
- base http.RoundTripper |
- log logging.Logger |
-} |
- |
-// RoundTrip appends authorization details to the request. |
-func (t *authTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { |
- tok := t.parent.currentToken() |
- if tok == nil || tok.Expired() { |
- tok, err = t.parent.refreshToken(tok) |
- if err != nil { |
- return |
- } |
- if tok == nil || tok.Expired() { |
- err = fmt.Errorf("auth: failed to refresh the token") |
- return |
- } |
- } |
- clone := *req |
- clone.Header = make(http.Header) |
- for k, v := range req.Header { |
- clone.Header[k] = v |
- } |
- for k, v := range tok.RequestHeaders() { |
- clone.Header.Set(k, v) |
- } |
- return t.base.RoundTrip(&clone) |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// tokenCache implementation. |
- |
-type tokenCache struct { |
- path string |
- log logging.Logger |
- lock sync.Mutex |
-} |
- |
-func (c *tokenCache) read() (buf []byte, err error) { |
- c.lock.Lock() |
- defer c.lock.Unlock() |
- c.log.Infof("auth: reading token from %s", c.path) |
- buf, err = ioutil.ReadFile(c.path) |
- if err != nil && os.IsNotExist(err) { |
- err = nil |
- } |
- return |
-} |
- |
-func (c *tokenCache) write(buf []byte) error { |
- c.lock.Lock() |
- defer c.lock.Unlock() |
- c.log.Infof("auth: writing token to %s", c.path) |
- err := os.MkdirAll(filepath.Dir(c.path), 0700) |
- if err != nil { |
- return err |
- } |
- // TODO(vadimsh): Make it atomic across multiple processes. |
- return ioutil.WriteFile(c.path, buf, 0600) |
-} |
- |
-func (c *tokenCache) clear() error { |
- c.lock.Lock() |
- defer c.lock.Unlock() |
- err := os.Remove(c.path) |
- if err != nil && !os.IsNotExist(err) { |
- return err |
- } |
- return nil |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// Utility functions. |
- |
-func cacheFileName(opts *Options) string { |
- // Construct a name of cache file from data that identifies requested |
- // credential, to allow multiple differently configured instances of |
- // Authenticator to coexist. |
- sum := sha1.New() |
- sum.Write([]byte(opts.Method)) |
- sum.Write([]byte{0}) |
- sum.Write([]byte(opts.ClientID)) |
- sum.Write([]byte{0}) |
- sum.Write([]byte(opts.ClientSecret)) |
- sum.Write([]byte{0}) |
- for _, scope := range opts.Scopes { |
- sum.Write([]byte(scope)) |
- sum.Write([]byte{0}) |
- } |
- sum.Write([]byte(opts.GCEAccountName)) |
- return hex.EncodeToString(sum.Sum(nil))[:16] |
-} |
- |
-// selectDefaultMethod is mocked in tests. |
-var selectDefaultMethod = func(opts *Options) Method { |
- if opts.ServiceAccountJSONPath != "" { |
- info, _ := os.Stat(opts.ServiceAccountJSONPath) |
- if info != nil && info.Mode().IsRegular() { |
- return ServiceAccountMethod |
- } |
- } |
- if metadata.OnGCE() { |
- return GCEMetadataMethod |
- } |
- return UserCredentialsMethod |
-} |
- |
-// secretsDir is mocked in tests. Called by publicly visible SecretsDir(). |
-var secretsDir = func() string { |
- usr, err := user.Current() |
- if err != nil { |
- panic(err.Error()) |
- } |
- // TODO(vadimsh): On Windows use SHGetFolderPath with CSIDL_LOCAL_APPDATA to |
- // locate a directory to store app files. |
- return filepath.Join(usr.HomeDir, ".config", "chrome_infra", "auth") |
-} |
- |
-// makeTokenProvider is mocked in tests. Called by ensureInitialized. |
-var makeTokenProvider = func(opts *Options) (internal.TokenProvider, error) { |
- switch opts.Method { |
- case UserCredentialsMethod: |
- return internal.NewUserAuthTokenProvider( |
- opts.Context, |
- opts.ClientID, |
- opts.ClientSecret, |
- opts.Scopes) |
- case ServiceAccountMethod: |
- return internal.NewServiceAccountTokenProvider( |
- opts.Context, |
- opts.ServiceAccountJSONPath, |
- opts.Scopes) |
- case GCEMetadataMethod: |
- return internal.NewGCETokenProvider( |
- opts.GCEAccountName, |
- opts.Scopes) |
- default: |
- return nil, fmt.Errorf("Unrecognized authentication method: %s", opts.Method) |
- } |
-} |
- |
-// DefaultClient returns OAuth client_id and client_secret to use for 3 legged |
-// OAuth flow. Note that client_secret is not really a secret since it's |
-// hardcoded into the source code (and binaries). It's totally fine, as long |
-// as it's callback URI is configured to be 'localhost'. If someone decides to |
-// reuse such client_secret they have to run something on user's local machine |
-// to get the refresh_token. |
-func DefaultClient() (clientID string, clientSecret string) { |
- clientID = "446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936.apps.googleusercontent.com" |
- clientSecret = "uBfbay2KCy9t4QveJ-dOqHtp" |
- return |
-} |
- |
-// SecretsDir returns an absolute path to a directory to keep secret files in. |
-func SecretsDir() string { |
- return secretsDir() |
-} |