1// Copyright 2015 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package google 6 7import ( 8 "encoding/json" 9 "errors" 10 "fmt" 11 "net/http" 12 "os" 13 "os/user" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "time" 18 19 "golang.org/x/net/context" 20 "golang.org/x/oauth2" 21 "golang.org/x/oauth2/internal" 22) 23 24type sdkCredentials struct { 25 Data []struct { 26 Credential struct { 27 ClientID string `json:"client_id"` 28 ClientSecret string `json:"client_secret"` 29 AccessToken string `json:"access_token"` 30 RefreshToken string `json:"refresh_token"` 31 TokenExpiry *time.Time `json:"token_expiry"` 32 } `json:"credential"` 33 Key struct { 34 Account string `json:"account"` 35 Scope string `json:"scope"` 36 } `json:"key"` 37 } 38} 39 40// An SDKConfig provides access to tokens from an account already 41// authorized via the Google Cloud SDK. 42type SDKConfig struct { 43 conf oauth2.Config 44 initialToken *oauth2.Token 45} 46 47// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK 48// account. If account is empty, the account currently active in 49// Google Cloud SDK properties is used. 50// Google Cloud SDK credentials must be created by running `gcloud auth` 51// before using this function. 52// The Google Cloud SDK is available at https://cloud.google.com/sdk/. 53func NewSDKConfig(account string) (*SDKConfig, error) { 54 configPath, err := sdkConfigPath() 55 if err != nil { 56 return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err) 57 } 58 credentialsPath := filepath.Join(configPath, "credentials") 59 f, err := os.Open(credentialsPath) 60 if err != nil { 61 return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err) 62 } 63 defer f.Close() 64 65 var c sdkCredentials 66 if err := json.NewDecoder(f).Decode(&c); err != nil { 67 return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err) 68 } 69 if len(c.Data) == 0 { 70 return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath) 71 } 72 if account == "" { 73 propertiesPath := filepath.Join(configPath, "properties") 74 f, err := os.Open(propertiesPath) 75 if err != nil { 76 return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err) 77 } 78 defer f.Close() 79 ini, err := internal.ParseINI(f) 80 if err != nil { 81 return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err) 82 } 83 core, ok := ini["core"] 84 if !ok { 85 return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini) 86 } 87 active, ok := core["account"] 88 if !ok { 89 return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core) 90 } 91 account = active 92 } 93 94 for _, d := range c.Data { 95 if account == "" || d.Key.Account == account { 96 if d.Credential.AccessToken == "" && d.Credential.RefreshToken == "" { 97 return nil, fmt.Errorf("oauth2/google: no token available for account %q", account) 98 } 99 var expiry time.Time 100 if d.Credential.TokenExpiry != nil { 101 expiry = *d.Credential.TokenExpiry 102 } 103 return &SDKConfig{ 104 conf: oauth2.Config{ 105 ClientID: d.Credential.ClientID, 106 ClientSecret: d.Credential.ClientSecret, 107 Scopes: strings.Split(d.Key.Scope, " "), 108 Endpoint: Endpoint, 109 RedirectURL: "oob", 110 }, 111 initialToken: &oauth2.Token{ 112 AccessToken: d.Credential.AccessToken, 113 RefreshToken: d.Credential.RefreshToken, 114 Expiry: expiry, 115 }, 116 }, nil 117 } 118 } 119 return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account) 120} 121 122// Client returns an HTTP client using Google Cloud SDK credentials to 123// authorize requests. The token will auto-refresh as necessary. The 124// underlying http.RoundTripper will be obtained using the provided 125// context. The returned client and its Transport should not be 126// modified. 127func (c *SDKConfig) Client(ctx context.Context) *http.Client { 128 return &http.Client{ 129 Transport: &oauth2.Transport{ 130 Source: c.TokenSource(ctx), 131 }, 132 } 133} 134 135// TokenSource returns an oauth2.TokenSource that retrieve tokens from 136// Google Cloud SDK credentials using the provided context. 137// It will returns the current access token stored in the credentials, 138// and refresh it when it expires, but it won't update the credentials 139// with the new access token. 140func (c *SDKConfig) TokenSource(ctx context.Context) oauth2.TokenSource { 141 return c.conf.TokenSource(ctx, c.initialToken) 142} 143 144// Scopes are the OAuth 2.0 scopes the current account is authorized for. 145func (c *SDKConfig) Scopes() []string { 146 return c.conf.Scopes 147} 148 149// sdkConfigPath tries to guess where the gcloud config is located. 150// It can be overridden during tests. 151var sdkConfigPath = func() (string, error) { 152 if runtime.GOOS == "windows" { 153 return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil 154 } 155 homeDir := guessUnixHomeDir() 156 if homeDir == "" { 157 return "", errors.New("unable to get current user home directory: os/user lookup failed; $HOME is empty") 158 } 159 return filepath.Join(homeDir, ".config", "gcloud"), nil 160} 161 162func guessUnixHomeDir() string { 163 // Prefer $HOME over user.Current due to glibc bug: golang.org/issue/13470 164 if v := os.Getenv("HOME"); v != "" { 165 return v 166 } 167 // Else, fall back to user.Current: 168 if u, err := user.Current(); err == nil { 169 return u.HomeDir 170 } 171 return "" 172} 173