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