Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 73 additions & 16 deletions google/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"cloud.google.com/go/compute/metadata"
"golang.org/x/oauth2"
"golang.org/x/oauth2/authhandler"
)

// Credentials holds Google credentials, including "Application Default Credentials".
Expand All @@ -41,6 +42,32 @@ type Credentials struct {
// Deprecated: use Credentials instead.
type DefaultCredentials = Credentials

// CredentialsParams holds user supplied parameters that are used together
// with a credentials file for building a Credentials object.
type CredentialsParams struct {
// Scopes is the list OAuth scopes. Required.
// Example: https://www.googleapis.com/auth/cloud-platform
Scopes []string

// Subject is the user email used for domain wide delegation (see
// https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
// Optional.
Subject string

// AuthHandler is the AuthorizationHandler used for 3-legged OAuth flow. Optional.
AuthHandler authhandler.AuthorizationHandler

// State is a unique string used with AuthHandler. Optional.
State string
}

func (params CredentialsParams) deepCopy() CredentialsParams {
paramsCopy := params
paramsCopy.Scopes = make([]string, len(params.Scopes))
copy(paramsCopy.Scopes, params.Scopes)
return paramsCopy
}

// DefaultClient returns an HTTP Client that uses the
// DefaultTokenSource to obtain authentication credentials.
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
Expand All @@ -62,7 +89,7 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc
return creds.TokenSource, nil
}

// FindDefaultCredentials searches for "Application Default Credentials".
// FindDefaultCredentialsWithParams searches for "Application Default Credentials".
//
// It looks for credentials in the following places,
// preferring the first location found:
Expand All @@ -81,11 +108,14 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc
// 4. On Google Compute Engine, Google App Engine standard second generation runtimes
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
// credentials from the metadata server.
func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) {
func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) {
// Make defensive copy of the slices in params.
params = params.deepCopy()

// First, try the environment variable.
const envVar = "GOOGLE_APPLICATION_CREDENTIALS"
if filename := os.Getenv(envVar); filename != "" {
creds, err := readCredentialsFile(ctx, filename, scopes)
creds, err := readCredentialsFile(ctx, filename, params)
if err != nil {
return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err)
}
Expand All @@ -94,7 +124,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials

// Second, try a well-known file.
filename := wellKnownFile()
if creds, err := readCredentialsFile(ctx, filename, scopes); err == nil {
if creds, err := readCredentialsFile(ctx, filename, params); err == nil {
return creds, nil
} else if !os.IsNotExist(err) {
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
Expand All @@ -106,7 +136,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
if appengineTokenFunc != nil {
return &DefaultCredentials{
ProjectID: appengineAppIDFunc(ctx),
TokenSource: AppEngineTokenSource(ctx, scopes...),
TokenSource: AppEngineTokenSource(ctx, params.Scopes...),
}, nil
}

Expand All @@ -116,7 +146,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
id, _ := metadata.ProjectID()
return &DefaultCredentials{
ProjectID: id,
TokenSource: ComputeTokenSource("", scopes...),
TokenSource: ComputeTokenSource("", params.Scopes...),
}, nil
}

Expand All @@ -125,18 +155,38 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
}

// CredentialsFromJSON obtains Google credentials from a JSON value. The JSON can
// represent either a Google Developers Console client_credentials.json file (as in
// ConfigFromJSON), a Google Developers service account key file (as in
// JWTConfigFromJSON) or the JSON configuration file for workload identity federation
// in non-Google cloud platforms (see
// https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes.
func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) {
var params CredentialsParams
params.Scopes = scopes
return FindDefaultCredentialsWithParams(ctx, params)
}

// CredentialsFromJSONWithParams obtains Google credentials from a JSON value. The JSON can
// represent either a Google Developers Console client_credentials.json file (as in ConfigFromJSON),
// a Google Developers service account key file, a gcloud user credentials file (a.k.a. refresh
// token JSON), or the JSON configuration file for workload identity federation in non-Google cloud
// platforms (see https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation).
func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) {
// Make defensive copy of the slices in params.
params = params.deepCopy()

// First, attempt to parse jsonData as a Google Developers Console client_credentials.json.
config, _ := ConfigFromJSON(jsonData, params.Scopes...)
if config != nil {
return &Credentials{
ProjectID: "",
TokenSource: authhandler.TokenSource(ctx, config, params.State, params.AuthHandler),
JSON: jsonData,
}, nil
}

// Otherwise, parse jsonData as one of the other supported credentials files.
var f credentialsFile
if err := json.Unmarshal(jsonData, &f); err != nil {
return nil, err
}
ts, err := f.tokenSource(ctx, append([]string(nil), scopes...))
ts, err := f.tokenSource(ctx, params)
if err != nil {
return nil, err
}
Expand All @@ -147,6 +197,13 @@ func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string)
}, nil
}

// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes.
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
var params CredentialsParams
params.Scopes = scopes
return CredentialsFromJSONWithParams(ctx, jsonData, params)
}

func wellKnownFile() string {
const f = "application_default_credentials.json"
if runtime.GOOS == "windows" {
Expand All @@ -155,10 +212,10 @@ func wellKnownFile() string {
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
}

func readCredentialsFile(ctx context.Context, filename string, scopes []string) (*DefaultCredentials, error) {
func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*DefaultCredentials, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return CredentialsFromJSON(ctx, b, scopes...)
return CredentialsFromJSONWithParams(ctx, b, params)
}
30 changes: 21 additions & 9 deletions google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"golang.org/x/oauth2/jwt"
)

// Endpoint is Google's OAuth 2.0 endpoint.
// Endpoint is Google's OAuth 2.0 default endpoint.
var Endpoint = oauth2.Endpoint{
AuthURL: "https://accounts.google.com/o/oauth2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
Expand Down Expand Up @@ -87,7 +87,7 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
}
scope = append([]string(nil), scope...) // copy
return f.jwtConfig(scope), nil
return f.jwtConfig(scope, ""), nil
}

// JSON key file types.
Expand All @@ -99,12 +99,13 @@ const (

// credentialsFile is the unmarshalled representation of a credentials file.
type credentialsFile struct {
Type string `json:"type"` // serviceAccountKey or userCredentialsKey
Type string `json:"type"`

// Service Account fields
ClientEmail string `json:"client_email"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
ProjectID string `json:"project_id"`

Expand All @@ -124,31 +125,42 @@ type credentialsFile struct {
QuotaProjectID string `json:"quota_project_id"`
}

func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config {
func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
cfg := &jwt.Config{
Email: f.ClientEmail,
PrivateKey: []byte(f.PrivateKey),
PrivateKeyID: f.PrivateKeyID,
Scopes: scopes,
TokenURL: f.TokenURL,
Subject: subject, // This is the user email to impersonate
}
if cfg.TokenURL == "" {
cfg.TokenURL = JWTTokenURL
}
return cfg
}

func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) {
func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
switch f.Type {
case serviceAccountKey:
cfg := f.jwtConfig(scopes)
cfg := f.jwtConfig(params.Scopes, params.Subject)
return cfg.TokenSource(ctx), nil
case userCredentialsKey:
cfg := &oauth2.Config{
ClientID: f.ClientID,
ClientSecret: f.ClientSecret,
Scopes: scopes,
Endpoint: Endpoint,
Scopes: params.Scopes,
Endpoint: oauth2.Endpoint{
AuthURL: f.AuthURL,
TokenURL: f.TokenURL,
AuthStyle: oauth2.AuthStyleInParams,
},
}
if cfg.Endpoint.AuthURL == "" {
cfg.Endpoint.AuthURL = Endpoint.AuthURL
}
if cfg.Endpoint.TokenURL == "" {
cfg.Endpoint.TokenURL = Endpoint.TokenURL
}
tok := &oauth2.Token{RefreshToken: f.RefreshToken}
return cfg.TokenSource(ctx, tok), nil
Expand All @@ -163,7 +175,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau
ClientID: f.ClientID,
CredentialSource: f.CredentialSource,
QuotaProjectID: f.QuotaProjectID,
Scopes: scopes,
Scopes: params.Scopes,
}
return cfg.TokenSource(ctx), nil
case "":
Expand Down