forked from Mirrors/oauth2
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>
This commit is contained in:
parent
95a9f97e51
commit
ab6e11b96c
|
@ -74,6 +74,19 @@ func ExampleJWTConfigFromJSON() {
|
||||||
client.Get("...")
|
client.Get("...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExampleSDKConfig() {
|
||||||
|
// The credentials will be obtained from the first account that
|
||||||
|
// has been authorized with `gcloud auth login`.
|
||||||
|
conf, err := google.NewSDKConfig("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
// Initiate an http.Client. The following GET request will be
|
||||||
|
// authorized and authenticated on the behalf of the SDK user.
|
||||||
|
client := conf.Client(oauth2.NoContext)
|
||||||
|
client.Get("...")
|
||||||
|
}
|
||||||
|
|
||||||
func Example_serviceAccount() {
|
func Example_serviceAccount() {
|
||||||
// Your credentials should be obtained from the Google
|
// Your credentials should be obtained from the Google
|
||||||
// Developer Console (https://console.developers.google.com).
|
// Developer Console (https://console.developers.google.com).
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sdkCredentials struct {
|
||||||
|
Data []struct {
|
||||||
|
Credential struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenExpiry time.Time `json:"token_expiry"`
|
||||||
|
} `json:"credential"`
|
||||||
|
Key struct {
|
||||||
|
Account string `json:"account"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
} `json:"key"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An SDKConfig provides access to tokens from an account already
|
||||||
|
// authorized via the Google Cloud SDK.
|
||||||
|
type SDKConfig struct {
|
||||||
|
conf oauth2.Config
|
||||||
|
initialToken *oauth2.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSDKConfig creates an SDKConfig for the given Google Cloud SDK
|
||||||
|
// account. If account is empty, the account currently active in
|
||||||
|
// Google Cloud SDK properties is used.
|
||||||
|
// Google Cloud SDK credentials must be created by running `gcloud auth`
|
||||||
|
// before using this function.
|
||||||
|
// The Google Cloud SDK is available at https://cloud.google.com/sdk/.
|
||||||
|
func NewSDKConfig(account string) (*SDKConfig, error) {
|
||||||
|
configPath, err := sdkConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: error getting SDK config path: %v", err)
|
||||||
|
}
|
||||||
|
credentialsPath := filepath.Join(configPath, "credentials")
|
||||||
|
f, err := os.Open(credentialsPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to load SDK credentials: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var c sdkCredentials
|
||||||
|
if err := json.NewDecoder(f).Decode(&c); err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to decode SDK credentials from %q: %v", credentialsPath, err)
|
||||||
|
}
|
||||||
|
if len(c.Data) == 0 {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no credentials found in %q, run `gcloud auth login` to create one", credentialsPath)
|
||||||
|
}
|
||||||
|
if account == "" {
|
||||||
|
propertiesPath := filepath.Join(configPath, "properties")
|
||||||
|
f, err := os.Open(propertiesPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to load SDK properties: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
ini, err := internal.ParseINI(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to parse SDK properties %q: %v", propertiesPath, err)
|
||||||
|
}
|
||||||
|
core, ok := ini["core"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to find [core] section in %v", ini)
|
||||||
|
}
|
||||||
|
active, ok := core["account"]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: failed to find %q attribute in %v", "account", core)
|
||||||
|
}
|
||||||
|
account = active
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range c.Data {
|
||||||
|
if account == "" || d.Key.Account == account {
|
||||||
|
return &SDKConfig{
|
||||||
|
conf: oauth2.Config{
|
||||||
|
ClientID: d.Credential.ClientID,
|
||||||
|
ClientSecret: d.Credential.ClientSecret,
|
||||||
|
Scopes: strings.Split(d.Key.Scope, " "),
|
||||||
|
Endpoint: Endpoint,
|
||||||
|
RedirectURL: "oob",
|
||||||
|
},
|
||||||
|
initialToken: &oauth2.Token{
|
||||||
|
AccessToken: d.Credential.AccessToken,
|
||||||
|
RefreshToken: d.Credential.RefreshToken,
|
||||||
|
Expiry: d.Credential.TokenExpiry,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("oauth2/google: no such credentials for account %q", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client returns an HTTP client using Google Cloud SDK credentials to
|
||||||
|
// authorize requests. The token will auto-refresh as necessary. The
|
||||||
|
// underlying http.RoundTripper will be obtained using the provided
|
||||||
|
// context. The returned client and its Transport should not be
|
||||||
|
// modified.
|
||||||
|
func (c *SDKConfig) Client(ctx oauth2.Context) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: &oauth2.Transport{
|
||||||
|
Source: c.TokenSource(ctx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenSource returns an oauth2.TokenSource that retrieve tokens from
|
||||||
|
// Google Cloud SDK credentials using the provided context.
|
||||||
|
// It will returns the current access token stored in the credentials,
|
||||||
|
// and refresh it when it expires, but it won't update the credentials
|
||||||
|
// with the new access token.
|
||||||
|
func (c *SDKConfig) TokenSource(ctx oauth2.Context) oauth2.TokenSource {
|
||||||
|
return c.conf.TokenSource(ctx, c.initialToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scopes are the OAuth 2.0 scopes the current account is authorized for.
|
||||||
|
func (c *SDKConfig) Scopes() []string {
|
||||||
|
return c.conf.Scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdkConfigPath() (string, error) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return filepath.Join(os.Getenv("APPDATA"), "gcloud"), nil
|
||||||
|
}
|
||||||
|
unixHomeDir = guessUnixHomeDir()
|
||||||
|
if unixHomeDir == "" {
|
||||||
|
return "", fmt.Errorf("unable to get current user home directory: os/user lookup failed; $HOME is empty")
|
||||||
|
}
|
||||||
|
return filepath.Join(unixHomeDir, ".config", "gcloud"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unixHomeDir string
|
||||||
|
|
||||||
|
func guessUnixHomeDir() string {
|
||||||
|
if unixHomeDir != "" {
|
||||||
|
return unixHomeDir
|
||||||
|
}
|
||||||
|
usr, err := user.Current()
|
||||||
|
if err == nil {
|
||||||
|
return usr.HomeDir
|
||||||
|
}
|
||||||
|
return os.Getenv("HOME")
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2015 The oauth2 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 google
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestSDKConfig(t *testing.T) {
|
||||||
|
unixHomeDir = "testdata"
|
||||||
|
tests := []struct {
|
||||||
|
account string
|
||||||
|
accessToken string
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
{"", "bar_access_token", false},
|
||||||
|
{"foo@example.com", "foo_access_token", false},
|
||||||
|
{"bar@example.com", "bar_access_token", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
c, err := NewSDKConfig(tt.account)
|
||||||
|
if (err != nil) != tt.err {
|
||||||
|
if !tt.err {
|
||||||
|
t.Errorf("expected no error, got error: %v", tt.err, err)
|
||||||
|
} else {
|
||||||
|
t.Errorf("execcted error, got none")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tok := c.initialToken
|
||||||
|
if tok == nil {
|
||||||
|
t.Errorf("expected token %q, got: nil", tt.accessToken)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if tok.AccessToken != tt.accessToken {
|
||||||
|
t.Errorf("expected token %q, got: %q", tt.accessToken, tok.AccessToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"credential": {
|
||||||
|
"_class": "OAuth2Credentials",
|
||||||
|
"_module": "oauth2client.client",
|
||||||
|
"access_token": "foo_access_token",
|
||||||
|
"client_id": "foo_client_id",
|
||||||
|
"client_secret": "foo_client_secret",
|
||||||
|
"id_token": {
|
||||||
|
"at_hash": "foo_at_hash",
|
||||||
|
"aud": "foo_aud",
|
||||||
|
"azp": "foo_azp",
|
||||||
|
"cid": "foo_cid",
|
||||||
|
"email": "foo@example.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 1420573614,
|
||||||
|
"iat": 1420569714,
|
||||||
|
"id": "1337",
|
||||||
|
"iss": "accounts.google.com",
|
||||||
|
"sub": "1337",
|
||||||
|
"token_hash": "foo_token_hash",
|
||||||
|
"verified_email": true
|
||||||
|
},
|
||||||
|
"invalid": false,
|
||||||
|
"refresh_token": "foo_refresh_token",
|
||||||
|
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"token_expiry": "2015-01-09T00:51:51Z",
|
||||||
|
"token_response": {
|
||||||
|
"access_token": "foo_access_token",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"id_token": "foo_id_token",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
},
|
||||||
|
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"user_agent": "Cloud SDK Command Line Tool"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"account": "foo@example.com",
|
||||||
|
"clientId": "foo_client_id",
|
||||||
|
"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",
|
||||||
|
"type": "google-cloud-sdk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credential": {
|
||||||
|
"_class": "OAuth2Credentials",
|
||||||
|
"_module": "oauth2client.client",
|
||||||
|
"access_token": "bar_access_token",
|
||||||
|
"client_id": "bar_client_id",
|
||||||
|
"client_secret": "bar_client_secret",
|
||||||
|
"id_token": {
|
||||||
|
"at_hash": "bar_at_hash",
|
||||||
|
"aud": "bar_aud",
|
||||||
|
"azp": "bar_azp",
|
||||||
|
"cid": "bar_cid",
|
||||||
|
"email": "bar@example.com",
|
||||||
|
"email_verified": true,
|
||||||
|
"exp": 1420573614,
|
||||||
|
"iat": 1420569714,
|
||||||
|
"id": "1337",
|
||||||
|
"iss": "accounts.google.com",
|
||||||
|
"sub": "1337",
|
||||||
|
"token_hash": "bar_token_hash",
|
||||||
|
"verified_email": true
|
||||||
|
},
|
||||||
|
"invalid": false,
|
||||||
|
"refresh_token": "bar_refresh_token",
|
||||||
|
"revoke_uri": "https://accounts.google.com/o/oauth2/revoke",
|
||||||
|
"token_expiry": "2015-01-09T00:51:51Z",
|
||||||
|
"token_response": {
|
||||||
|
"access_token": "bar_access_token",
|
||||||
|
"expires_in": 3600,
|
||||||
|
"id_token": "bar_id_token",
|
||||||
|
"token_type": "Bearer"
|
||||||
|
},
|
||||||
|
"token_uri": "https://accounts.google.com/o/oauth2/token",
|
||||||
|
"user_agent": "Cloud SDK Command Line Tool"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"account": "bar@example.com",
|
||||||
|
"clientId": "bar_client_id",
|
||||||
|
"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",
|
||||||
|
"type": "google-cloud-sdk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"file_version": 1
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
[core]
|
||||||
|
account = bar@example.com
|
|
@ -6,10 +6,14 @@
|
||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseKey converts the binary contents of a private key file
|
// ParseKey converts the binary contents of a private key file
|
||||||
|
@ -35,3 +39,31 @@ func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
||||||
}
|
}
|
||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseINI(ini io.Reader) (map[string]map[string]string, error) {
|
||||||
|
result := map[string]map[string]string{
|
||||||
|
"": map[string]string{}, // root section
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(ini)
|
||||||
|
currentSection := ""
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, ";") {
|
||||||
|
// comment.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||||
|
currentSection = strings.TrimSpace(line[1 : len(line)-1])
|
||||||
|
result[currentSection] = map[string]string{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 && parts[0] != "" {
|
||||||
|
result[currentSection][strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error scanning ini: %v", err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2014 The oauth2 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 internal contains support packages for oauth2 package.
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseINI(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ini string
|
||||||
|
want map[string]map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`root = toor
|
||||||
|
[foo]
|
||||||
|
bar = hop
|
||||||
|
ini = nin
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"": map[string]string{"root": "toor"},
|
||||||
|
"foo": map[string]string{"bar": "hop", "ini": "nin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[empty]
|
||||||
|
[section]
|
||||||
|
empty=
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"": map[string]string{},
|
||||||
|
"empty": map[string]string{},
|
||||||
|
"section": map[string]string{"empty": ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`ignore
|
||||||
|
[invalid
|
||||||
|
=stuff
|
||||||
|
;comment=true
|
||||||
|
`,
|
||||||
|
map[string]map[string]string{
|
||||||
|
"a": map[string]string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
result, err := ParseINI(strings.NewReader(tt.ini))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ParseINI(%q) error %v, want: no error", tt.ini, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result, tt.want) {
|
||||||
|
t.Errorf("ParseINI(%q) = %#v, want: %#v", tt.ini, result, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue