Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,22 @@ client, err := sheets.NewService(ctx)
```

To authorize using a [JSON key file](https://cloud.google.com/iam/docs/managing-service-account-keys), pass
[`option.WithCredentialsFile`](https://pkg.go.dev/google.golang.org/api/option#WithCredentialsFile) to the `NewService`
function of the desired package. For example:
[`option.WithAuthCredentialsFile`](https://pkg.go.dev/google.golang.org/api/option#WithAuthCredentialsFile) to the `NewService`
function of the desired package. You must also specify the credential type. For example, to use a service account key file:

```go
client, err := sheets.NewService(ctx, option.WithCredentialsFile("path/to/keyfile.json"))
client, err := sheets.NewService(ctx, option.WithAuthCredentialsFile(option.ServiceAccount, "path/to/keyfile.json"))
```

Similarly, you can use JSON credentials directly with [`option.WithAuthCredentialsJSON`](https://pkg.go.dev/google.golang.org/api/option#WithAuthCredentialsJSON):

```go
// where jsonKey is a []byte containing the JSON key
client, err := sheets.NewService(ctx, option.WithAuthCredentialsJSON(option.ServiceAccount, jsonKey))
```

The older `option.WithCredentialsFile` and `option.WithCredentialsJSON` functions are deprecated due to a potential security risk.

You can exert more control over authorization by using the [`golang.org/x/oauth2`](https://pkg.go.dev/golang.org/x/oauth2)
package to create an `oauth2.TokenSource`. Then pass [`option.WithTokenSource`](https://pkg.go.dev/google.golang.org/api/option#WithTokenSource)
to the `NewService` function:
Expand Down
157 changes: 140 additions & 17 deletions idtoken/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,40 @@ import (
// ClientOption is for configuring a Google API client or transport.
type ClientOption = option.ClientOption

type credentialsType int
// CredentialsType specifies the type of JSON credentials being provided
// to a loading function such as [WithAuthCredentialsFile] or
// [WithAuthCredentialsJSON].
type CredentialsType = option.CredentialsType

const (
unknownCredType credentialsType = iota
serviceAccount
impersonatedServiceAccount
externalAccount
// unknownCredType is a private CredentialsType representing an unknown JSON file type.
unknownCredType = internal.Unknown
// ServiceAccount represents a service account file type.
ServiceAccount = option.ServiceAccount
// User represents a user credentials file type.
User = option.User
// ImpersonatedServiceAccount represents an impersonated service account file type.
//
// IMPORTANT:
// This credential type does not validate the credential configuration. A security
// risk occurs when a credential configuration configured with malicious urls
// is used.
// You should validate credential configurations provided by untrusted sources.
// See [Security requirements when using credential configurations from an external
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
// for more details.
ImpersonatedServiceAccount = option.ImpersonatedServiceAccount
// ExternalAccount represents an external account file type.
//
// IMPORTANT:
// This credential type does not validate the credential configuration. A security
// risk occurs when a credential configuration configured with malicious urls
// is used.
// You should validate credential configurations provided by untrusted sources.
// See [Security requirements when using credential configurations from an external
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
// for more details.
ExternalAccount = option.ExternalAccount
)

// NewClient creates a HTTP Client that automatically adds an ID token to each
Expand Down Expand Up @@ -110,11 +137,13 @@ func newTokenSourceNewAuth(ctx context.Context, audience string, ds *internal.Di
if ds.AuthCredentials != nil {
return nil, fmt.Errorf("idtoken: option.WithTokenProvider not supported")
}
credsJSON, _ := ds.GetAuthCredentialsJSON()
credsFile, _ := ds.GetAuthCredentialsFile()
creds, err := newidtoken.NewCredentials(&newidtoken.Options{
Audience: audience,
CustomClaims: ds.CustomClaims,
CredentialsFile: ds.CredentialsFile,
CredentialsJSON: ds.CredentialsJSON,
CredentialsFile: credsFile,
CredentialsJSON: credsJSON,
Client: oauth2.NewClient(ctx, nil),
Logger: ds.Logger,
})
Expand Down Expand Up @@ -146,7 +175,7 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds
return nil, err
}
switch allowedType {
case serviceAccount:
case ServiceAccount:
cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...)
if err != nil {
return nil, err
Expand All @@ -166,7 +195,7 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds
return nil, err
}
return oauth2.ReuseTokenSource(tok, ts), nil
case impersonatedServiceAccount, externalAccount:
case ImpersonatedServiceAccount, ExternalAccount:
type url struct {
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
}
Expand All @@ -182,20 +211,20 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds
TargetPrincipal: account,
IncludeEmail: true,
}
ts, err := impersonate.IDTokenSource(ctx, config, option.WithCredentialsJSON(data))
ts, err := impersonate.IDTokenSource(ctx, config, option.WithAuthCredentialsJSON(allowedType, data))
if err != nil {
return nil, err
}
return ts, nil
default:
return nil, fmt.Errorf("idtoken: unsupported credentials type")
return nil, fmt.Errorf("idtoken: unsupported credentials type: %d", allowedType)
}
}

// getAllowedType returns the credentials type of type credentialsType, and an error.
// allowed types are "service_account" and "impersonated_service_account"
func getAllowedType(data []byte) (credentialsType, error) {
var t credentialsType
func getAllowedType(data []byte) (CredentialsType, error) {
var t CredentialsType
if len(data) == 0 {
return t, fmt.Errorf("idtoken: credential provided is 0 bytes")
}
Expand All @@ -209,14 +238,14 @@ func getAllowedType(data []byte) (credentialsType, error) {
return t, nil
}

func parseCredType(typeString string) credentialsType {
func parseCredType(typeString string) CredentialsType {
switch typeString {
case "service_account":
return serviceAccount
return ServiceAccount
case "impersonated_service_account":
return impersonatedServiceAccount
return ImpersonatedServiceAccount
case "external_account":
return externalAccount
return ExternalAccount
default:
return unknownCredType
}
Expand All @@ -236,17 +265,111 @@ func (w withCustomClaims) Apply(o *internal.DialSettings) {
// WithCredentialsFile returns a ClientOption that authenticates
// API calls with the given service account or refresh token JSON
// credentials file.
//
// Important: If you accept a credential configuration (credential
// JSON/File/Stream) from an external source for authentication to Google
// Cloud Platform, you must validate it before providing it to any Google
// API or library. Providing an unvalidated credential configuration to
// Google APIs can compromise the security of your systems and data. For
// more information, refer to [Validate credential configurations from
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
//
// Deprecated: This function is being deprecated because of a potential security risk.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this go's way of deprecating?

No tags or something like that?

//
// This function does not validate the credential configuration. The security
// risk occurs when a credential configuration is accepted from a source that
// is not under your control and used without validation on your side.
//
// If you know that you will be loading credential configurations of a
// specific type, it is recommended to use a credential-type-specific
// option function.
// This will ensure that an unexpected credential type with potential for
// malicious intent is not loaded unintentionally. You might still have to do
// validation for certain credential types. Please follow the recommendation
// for that function. For example, if you want to load only service accounts,
// you can use [WithAuthCredentialsFile] with [ServiceAccount]:
// ```
// option.WithAuthCredentialsFile(option.ServiceAccount, "/path/to/file.json")
// ```
//
// If you are loading your credential configuration from an untrusted source and have
// not mitigated the risks (e.g. by validating the configuration yourself), make
// these changes as soon as possible to prevent security risks to your environment.
//
// Regardless of the function used, it is always your responsibility to validate
// configurations received from external sources.
func WithCredentialsFile(filename string) ClientOption {
return option.WithCredentialsFile(filename)
}

// WithAuthCredentialsFile returns a ClientOption that authenticates API calls
// with the given JSON credentials file and credential type.
//
// Important: If you accept a credential configuration (credential
// JSON/File/Stream) from an external source for authentication to Google
// Cloud Platform, you must validate it before providing it to any Google
// API or library. Providing an unvalidated credential configuration to
// Google APIs can compromise the security of your systems and data. For
// more information, refer to [Validate credential configurations from
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
func WithAuthCredentialsFile(credType CredentialsType, filename string) ClientOption {
return option.WithAuthCredentialsFile(credType, filename)
}

// WithCredentialsJSON returns a ClientOption that authenticates
// API calls with the given service account or refresh token JSON
// credentials.
//
// Important: If you accept a credential configuration (credential
// JSON/File/Stream) from an external source for authentication to Google
// Cloud Platform, you must validate it before providing it to any Google
// API or library. Providing an unvalidated credential configuration to
// Google APIs can compromise the security of your systems and data. For
// more information, refer to [Validate credential configurations from
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
//
// Deprecated: This function is being deprecated because of a potential security risk.
//
// This function does not validate the credential configuration. The security
// risk occurs when a credential configuration is accepted from a source that
// is not under your control and used without validation on your side.
//
// If you know that you will be loading credential configurations of a
// specific type, it is recommended to use a credential-type-specific
// option function.
// This will ensure that an unexpected credential type with potential for
// malicious intent is not loaded unintentionally. You might still have to do
// validation for certain credential types. Please follow the recommendation
// for that function. For example, if you want to load only service accounts,
// you can use [WithAuthCredentialsJSON] with [ServiceAccount]:
// ```
// option.WithAuthCredentialsJSON(option.ServiceAccount, json)
// ```
//
// If you are loading your credential configuration from an untrusted source and have
// not mitigated the risks (e.g. by validating the configuration yourself), make
// these changes as soon as possible to prevent security risks to your environment.
//
// Regardless of the function used, it is always your responsibility to validate
// configurations received from external sources.
func WithCredentialsJSON(p []byte) ClientOption {
return option.WithCredentialsJSON(p)
}

// WithAuthCredentialsJSON returns a ClientOption that authenticates API calls
// with the given JSON credentials and credential type.
//
// Important: If you accept a credential configuration (credential
// JSON/File/Stream) from an external source for authentication to Google
// Cloud Platform, you must validate it before providing it to any Google
// API or library. Providing an unvalidated credential configuration to
// Google APIs can compromise the security of your systems and data. For
// more information, refer to [Validate credential configurations from
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
func WithAuthCredentialsJSON(credType CredentialsType, json []byte) ClientOption {
return option.WithAuthCredentialsJSON(credType, json)
}

// WithHTTPClient returns a ClientOption that specifies the HTTP client to use
// as the basis of communications. This option may only be used with services
// that support HTTP as their communication transport. When used, the
Expand Down
38 changes: 6 additions & 32 deletions idtoken/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
"strings"
"testing"

"golang.org/x/oauth2/google"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
)

const (
envCredentialFile = "GOOGLE_APPLICATION_CREDENTIALS"
// Change this type as needed to match the credentials type of GOOGLE_APPLICATION_CREDENTIALS JSON or ADC credentials JSON.
credentialsFileType = idtoken.ServiceAccount

aud = "http://example.com"
)
Expand All @@ -26,38 +27,11 @@ func TestNewTokenSource(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ts, err := idtoken.NewTokenSource(context.Background(), "http://example.com", option.WithCredentialsFile(os.Getenv(envCredentialFile)))
if err != nil {
t.Fatalf("unable to create TokenSource: %v", err)
}
tok, err := ts.Token()
if err != nil {
t.Fatalf("unable to retrieve Token: %v", err)
}
req := &http.Request{Header: make(http.Header)}
tok.SetAuthHeader(req)
if !strings.HasPrefix(req.Header.Get("Authorization"), "Bearer ") {
t.Fatalf("token should sign requests with Bearer Authorization header")
}
validTok, err := idtoken.Validate(context.Background(), tok.AccessToken, aud)
if err != nil {
t.Fatalf("token validation failed: %v", err)
}
if validTok.Audience != aud {
t.Fatalf("got %q, want %q", validTok.Audience, aud)
}
}

func TestNewTokenSource_WithCredentialJSON(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
creds, err := google.FindDefaultCredentials(ctx)
if err != nil {
t.Fatalf("unable to find default creds: %v", err)
credsPath := os.Getenv(envCredentialFile)
if credsPath == "" {
t.Fatalf("Env var is not set: %s", envCredentialFile)
}
ts, err := idtoken.NewTokenSource(ctx, aud, option.WithCredentialsJSON(creds.JSON))
ts, err := idtoken.NewTokenSource(context.Background(), "http://example.com", option.WithAuthCredentialsFile(credentialsFileType, credsPath))
if err != nil {
t.Fatalf("unable to create Client: %v", err)
}
Expand Down
6 changes: 3 additions & 3 deletions impersonate/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func TestCredentialsTokenSourceIntegration(t *testing.T) {
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
Delegates: tt.delegates,
},
option.WithCredentialsFile(tt.baseKeyFile),
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
)
if err != nil {
t.Fatalf("failed to create ts: %v", err)
Expand Down Expand Up @@ -143,7 +143,7 @@ func TestIDTokenSourceIntegration(t *testing.T) {
Delegates: tt.delegates,
IncludeEmail: true,
},
option.WithCredentialsFile(tt.baseKeyFile),
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
)
if err != nil {
t.Fatalf("failed to create ts: %v", err)
Expand Down Expand Up @@ -198,7 +198,7 @@ func TestTokenSourceIntegration_user(t *testing.T) {
Scopes: []string{"https://www.googleapis.com/auth/admin.directory.user", "https://www.googleapis.com/auth/admin.directory.group"},
Subject: domainAdmin,
},
option.WithCredentialsFile(baseKeyFile),
option.WithAuthCredentialsFile(option.ServiceAccount, baseKeyFile),
)
if err != nil {
t.Fatalf("failed to create ts: %v", err)
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/byoid/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func getClientID(keyFileName string) (string, error) {
}

func generateGoogleToken(keyFileName string) (string, error) {
ts, err := idtoken.NewTokenSource(context.Background(), oidcAudience, option.WithCredentialsFile(keyFileName))
ts, err := idtoken.NewTokenSource(context.Background(), oidcAudience, option.WithAuthCredentialsFile(option.ServiceAccount, keyFileName))
if err != nil {
return "", nil
}
Expand Down Expand Up @@ -175,7 +175,7 @@ func testBYOID(t *testing.T, c config) {
writeConfig(t, c, func(name string) {
// Once the default credentials are obtained,
// we should be able to access Google Cloud resources.
dnsService, err := dns.NewService(context.Background(), option.WithCredentialsFile(name))
dnsService, err := dns.NewService(context.Background(), option.WithAuthCredentialsFile(option.ExternalAccount, name))
if err != nil {
t.Fatalf("Could not establish DNS Service: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/downscope/downscope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestMain(m *testing.M) {
credentialFileName := os.Getenv(envServiceAccountFile)

var err error
rootCredential, err = transport.Creds(ctx, option.WithCredentialsFile(credentialFileName), option.WithScopes(rootTokenScope))
rootCredential, err = transport.Creds(ctx, option.WithAuthCredentialsFile(option.ServiceAccount, credentialFileName), option.WithScopes(rootTokenScope))

if err != nil {
log.Fatalf("failed to construct root credential: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/impersonate/impersonate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestImpersonatedCredentials(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc, err := storage.NewService(ctx,
option.WithCredentialsFile(tt.baseSALocation),
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseSALocation),
option.ImpersonateCredentials(writerSA, tt.delgates...),
)
if err != nil {
Expand Down
Loading