Skip to content

Commit ab6e11b

Browse files
proppyrakyll
authored andcommitted
oauth2/google: add config type to use Cloud SDK credentials
Change-Id: Ied7fecc0cb155c33faca7766b81221eacb3aa0c0 Reviewed-on: https://go-review.googlesource.com/1670 Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> Reviewed-by: Burcu Dogan <jbd@google.com>
1 parent 95a9f97 commit ab6e11b

File tree

7 files changed

+399
-0
lines changed

7 files changed

+399
-0
lines changed

google/example_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,19 @@ func ExampleJWTConfigFromJSON() {
7474
client.Get("...")
7575
}
7676

77+
func ExampleSDKConfig() {
78+
// The credentials will be obtained from the first account that
79+
// has been authorized with `gcloud auth login`.
80+
conf, err := google.NewSDKConfig("")
81+
if err != nil {
82+
log.Fatal(err)
83+
}
84+
// Initiate an http.Client. The following GET request will be
85+
// authorized and authenticated on the behalf of the SDK user.
86+
client := conf.Client(oauth2.NoContext)
87+
client.Get("...")
88+
}
89+
7790
func Example_serviceAccount() {
7891
// Your credentials should be obtained from the Google
7992
// Developer Console (https://console.developers.google.com).

google/sdk.go

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

google/sdk_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright 2015 The oauth2 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 "testing"
8+
9+
func TestSDKConfig(t *testing.T) {
10+
unixHomeDir = "testdata"
11+
tests := []struct {
12+
account string
13+
accessToken string
14+
err bool
15+
}{
16+
{"", "bar_access_token", false},
17+
{"foo@example.com", "foo_access_token", false},
18+
{"bar@example.com", "bar_access_token", false},
19+
}
20+
for _, tt := range tests {
21+
c, err := NewSDKConfig(tt.account)
22+
if (err != nil) != tt.err {
23+
if !tt.err {
24+
t.Errorf("expected no error, got error: %v", tt.err, err)
25+
} else {
26+
t.Errorf("execcted error, got none")
27+
}
28+
continue
29+
}
30+
tok := c.initialToken
31+
if tok == nil {
32+
t.Errorf("expected token %q, got: nil", tt.accessToken)
33+
continue
34+
}
35+
if tok.AccessToken != tt.accessToken {
36+
t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken)
37+
}
38+
}
39+
}
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
{
2+
"data": [
3+
{
4+
"credential": {
5+
"_class": "OAuth2Credentials",
6+
"_module": "oauth2client.client",
7+
"access_token": "foo_access_token",
8+
"client_id": "foo_client_id",
9+
"client_secret": "foo_client_secret",
10+
"id_token": {
11+
"at_hash": "foo_at_hash",
12+
"aud": "foo_aud",
13+
"azp": "foo_azp",
14+
"cid": "foo_cid",
15+
"email": "foo@example.com",
16+
"email_verified": true,
17+
"exp": 1420573614,
18+
"iat": 1420569714,
19+
"id": "1337",
20+
"iss": "accounts.google.com",
21+
"sub": "1337",
22+
"token_hash": "foo_token_hash",
23+
"verified_email": true
24+
},
25+
"invalid": false,
26+
"refresh_token": "foo_refresh_token",
27+
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
28+
"token_expiry": "2015-01-09T00:51:51Z",
29+
"token_response": {
30+
"access_token": "foo_access_token",
31+
"expires_in": 3600,
32+
"id_token": "foo_id_token",
33+
"token_type": "Bearer"
34+
},
35+
"token_uri": "https://accounts.google.com/o/oauth2/token",
36+
"user_agent": "Cloud SDK Command Line Tool"
37+
},
38+
"key": {
39+
"account": "foo@example.com",
40+
"clientId": "foo_client_id",
41+
"scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
42+
"type": "google-cloud-sdk"
43+
}
44+
},
45+
{
46+
"credential": {
47+
"_class": "OAuth2Credentials",
48+
"_module": "oauth2client.client",
49+
"access_token": "bar_access_token",
50+
"client_id": "bar_client_id",
51+
"client_secret": "bar_client_secret",
52+
"id_token": {
53+
"at_hash": "bar_at_hash",
54+
"aud": "bar_aud",
55+
"azp": "bar_azp",
56+
"cid": "bar_cid",
57+
"email": "bar@example.com",
58+
"email_verified": true,
59+
"exp": 1420573614,
60+
"iat": 1420569714,
61+
"id": "1337",
62+
"iss": "accounts.google.com",
63+
"sub": "1337",
64+
"token_hash": "bar_token_hash",
65+
"verified_email": true
66+
},
67+
"invalid": false,
68+
"refresh_token": "bar_refresh_token",
69+
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
70+
"token_expiry": "2015-01-09T00:51:51Z",
71+
"token_response": {
72+
"access_token": "bar_access_token",
73+
"expires_in": 3600,
74+
"id_token": "bar_id_token",
75+
"token_type": "Bearer"
76+
},
77+
"token_uri": "https://accounts.google.com/o/oauth2/token",
78+
"user_agent": "Cloud SDK Command Line Tool"
79+
},
80+
"key": {
81+
"account": "bar@example.com",
82+
"clientId": "bar_client_id",
83+
"scope": "https://www.googleapis.com/auth/appengine.admin https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/compute https://www.googleapis.com/auth/devstorage.full_control https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/ndev.cloudman https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.admin https://www.googleapis.com/auth/prediction https://www.googleapis.com/auth/projecthosting",
84+
"type": "google-cloud-sdk"
85+
}
86+
}
87+
],
88+
"file_version": 1
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[core]
2+
account = bar@example.com

internal/oauth2.go

+32
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
package internal
77

88
import (
9+
"bufio"
910
"crypto/rsa"
1011
"crypto/x509"
1112
"encoding/pem"
1213
"errors"
14+
"fmt"
15+
"io"
16+
"strings"
1317
)
1418

1519
// ParseKey converts the binary contents of a private key file
@@ -35,3 +39,31 @@ func ParseKey(key []byte) (*rsa.PrivateKey, error) {
3539
}
3640
return parsed, nil
3741
}
42+
43+
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
44+
result := map[string]map[string]string{
45+
"": map[string]string{}, // root section
46+
}
47+
scanner := bufio.NewScanner(ini)
48+
currentSection := ""
49+
for scanner.Scan() {
50+
line := strings.TrimSpace(scanner.Text())
51+
if strings.HasPrefix(line, ";") {
52+
// comment.
53+
continue
54+
}
55+
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
56+
currentSection = strings.TrimSpace(line[1 : len(line)-1])
57+
result[currentSection] = map[string]string{}
58+
continue
59+
}
60+
parts := strings.SplitN(line, "=", 2)
61+
if len(parts) == 2 && parts[0] != "" {
62+
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
63+
}
64+
}
65+
if err := scanner.Err(); err != nil {
66+
return nil, fmt.Errorf("error scanning ini: %v", err)
67+
}
68+
return result, nil
69+
}

0 commit comments

Comments
 (0)