[oauth2] google: add safer credentials JSON loading options.

1 view
Skip to first unread message

Chris Smith (Gerrit)

unread,
Dec 23, 2025, 4:50:36 PM (2 days ago) Dec 23
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Chris Smith has uploaded the change for review

Commit message

google: add safer credentials JSON loading options.

Add safer credentials JSON loading options in `google` package.

Adds `CredentialsFromJSONWithType` and `CredentialsFromJSONWithTypeAndParams`
to mitigate a security vulnerability where credential configurations
from untrusted sources could be used without validation. These new
functions require the credential type to be explicitly specified.

Deprecates the less safe `CredentialsFromJSON` and
`CredentialsFromJSONWithParams` functions.
Change-Id: I27848b5ebd2dff76d0397cdc08908d680c0ccd69

Change diff

diff --git a/google/default.go b/google/default.go
index 0260935..6e57206 100644
--- a/google/default.go
+++ b/google/default.go
@@ -153,6 +153,43 @@
return paramsCopy
}

+// CredentialsType specifies the type of JSON credentials being provided
+// to a loading function.
+type CredentialsType string
+
+const (
+ // ServiceAccount represents a service account file type.
+ ServiceAccount CredentialsType = "service_account"
+ // AuthorizedUser represents a user credentials file type.
+ AuthorizedUser CredentialsType = "authorized_user"
+ // 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 CredentialsType = "external_account"
+ // ExternalAccountAuthorizedUser represents an external account authorized user file type.
+ ExternalAccountAuthorizedUser CredentialsType = "external_account_authorized_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 CredentialsType = "impersonated_service_account"
+ // GDCHServiceAccount represents a GDCH service account credentials.
+ GDCHServiceAccount CredentialsType = "gdch_service_account"
+)
+
// DefaultClient returns an HTTP Client that uses the
// DefaultTokenSource to obtain authentication credentials.
func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) {
@@ -246,17 +283,71 @@
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).
+// CredentialsFromJSONWithType invokes CredentialsFromJSONWithTypeAndParams with the specified scopes.
//
// 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 CredentialsFromJSONWithType(ctx context.Context, jsonData []byte, credType CredentialsType, scopes ...string) (*Credentials, error) {
+ var params CredentialsParams
+ params.Scopes = scopes
+ return CredentialsFromJSONWithTypeAndParams(ctx, jsonData, credType, params)
+}
+
+// CredentialsFromJSONWithTypeAndParams obtains Google credentials from a JSON value and
+// validates that the credentials match the specified 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 CredentialsFromJSONWithTypeAndParams(ctx context.Context, jsonData []byte, credType CredentialsType, params CredentialsParams) (*Credentials, error) {
+ var f struct {
+ Type string `json:"type"`
+ }
+ if err := json.Unmarshal(jsonData, &f); err != nil {
+ return nil, err
+ }
+ if CredentialsType(f.Type) != credType {
+ return nil, fmt.Errorf("google: expected credential type %q, found %q", credType, f.Type)
+ }
+ return CredentialsFromJSONWithParams(ctx, jsonData, 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).
+//
+// Deprecated: This function is deprecated because of a potential security risk.
+// It 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
+// CredentialsFromJSONWithTypeAndParams method. 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 method. For
+// example, if you want to load only service accounts, you can use
+//
+// creds, err := google.CredentialsFromJSONWithTypeAndParams(ctx, jsonData, google.ServiceAccount, params)
+//
+// 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 method used, it is always your responsibility to
+// validate configurations received from external sources.
+//
+// For more details see:
+// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) {
// Make defensive copy of the slices in params.
params = params.deepCopy()
@@ -301,11 +392,31 @@

// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes.
//
-// 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 deprecated because of a potential security risk.
+// It 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
+// CredentialsFromJSONWithType method. 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 method. For
+// example, if you want to load only service accounts, you can use
+//
+// creds, err := google.CredentialsFromJSONWithType(ctx, jsonData, google.ServiceAccount, scopes...)
+//
+// 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 method used, it is always your responsibility to
+// validate configurations received from external sources.
+//
+// For more details see:
+// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) {
var params CredentialsParams
params.Scopes = scopes
diff --git a/google/default_test.go b/google/default_test.go
index c8465e9..8b64058 100644
--- a/google/default_test.go
+++ b/google/default_test.go
@@ -8,6 +8,7 @@
"context"
"net/http"
"net/http/httptest"
+ "os"
"strings"
"testing"

@@ -310,3 +311,88 @@
}

}
+
+func TestCredentialsFromJSONWithType(t *testing.T) {
+ ctx := context.Background()
+ sa, err := os.ReadFile("testdata/sa.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ user, err := os.ReadFile("testdata/user.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ gdch, err := os.ReadFile("testdata/gdch.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ tests := []struct {
+ name string
+ credType CredentialsType
+ json []byte
+ wantErr bool
+ wantErrMsg string
+ }{
+ {
+ name: "ServiceAccount Success",
+ credType: ServiceAccount,
+ json: sa,
+ wantErr: false,
+ },
+ {
+ name: "User Success",
+ credType: AuthorizedUser,
+ json: user,
+ wantErr: false,
+ },
+ {
+ name: "GDCH Success",
+ credType: GDCHServiceAccount,
+ json: gdch,
+ wantErr: false,
+ },
+ {
+ name: "ServiceAccount Mismatch",
+ credType: ServiceAccount,
+ json: user,
+ wantErr: true,
+ wantErrMsg: `google: expected credential type "service_account", found "authorized_user"`,
+ },
+ {
+ name: "User Mismatch",
+ credType: AuthorizedUser,
+ json: sa,
+ wantErr: true,
+ wantErrMsg: `google: expected credential type "authorized_user", found "service_account"`,
+ },
+ {
+ name: "Malformed JSON",
+ credType: ServiceAccount,
+ json: []byte(`{"type": "service_account",}`),
+ wantErr: true,
+ wantErrMsg: "invalid character",
+ },
+ {
+ name: "Missing Type Field",
+ credType: ServiceAccount,
+ json: []byte(`{"project_id": "my-proj"}`),
+ wantErr: true,
+ wantErrMsg: `google: expected credential type "service_account", found ""`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := CredentialsFromJSONWithType(ctx, tt.json, tt.credType)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("CredentialsFromJSONWithType() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ if tt.wantErr {
+ if !strings.Contains(err.Error(), tt.wantErrMsg) {
+ t.Errorf("CredentialsFromJSONWithType() error = %q, want error containing %q", err.Error(), tt.wantErrMsg)
+ }
+ return
+ }
+ })
+ }
+}
diff --git a/google/google.go b/google/google.go
index 7d1fdd3..14c98eb 100644
--- a/google/google.go
+++ b/google/google.go
@@ -103,6 +103,7 @@
externalAccountKey = "external_account"
externalAccountAuthorizedUserKey = "external_account_authorized_user"
impersonatedServiceAccount = "impersonated_service_account"
+ gdchServiceAccountKey = "gdch_service_account"
)

// credentialsFile is the unmarshalled representation of a credentials file.
@@ -165,7 +166,7 @@

func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) {
switch f.Type {
- case serviceAccountKey:
+ case serviceAccountKey, gdchServiceAccountKey:
cfg := f.jwtConfig(params.Scopes, params.Subject)
return cfg.TokenSource(ctx), nil
case userCredentialsKey:
diff --git a/google/testdata/gdch.json b/google/testdata/gdch.json
new file mode 100644
index 0000000..2440348
--- /dev/null
+++ b/google/testdata/gdch.json
@@ -0,0 +1,3 @@
+{
+ "type": "gdch_service_account"
+}
\ No newline at end of file
diff --git a/google/testdata/sa.json b/google/testdata/sa.json
new file mode 100644
index 0000000..754478d
--- /dev/null
+++ b/google/testdata/sa.json
@@ -0,0 +1,3 @@
+{
+ "type": "service_account"
+}
\ No newline at end of file
diff --git a/google/testdata/user.json b/google/testdata/user.json
new file mode 100644
index 0000000..ca09fa5
--- /dev/null
+++ b/google/testdata/user.json
@@ -0,0 +1,3 @@
+{
+ "type": "authorized_user"
+}
\ No newline at end of file

Change information

Files:
  • M google/default.go
  • M google/default_test.go
  • M google/google.go
  • A google/testdata/gdch.json
  • A google/testdata/sa.json
  • A google/testdata/user.json
Change size: M
Delta: 6 files changed, 218 insertions(+), 11 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: oauth2
Gerrit-Branch: master
Gerrit-Change-Id: I27848b5ebd2dff76d0397cdc08908d680c0ccd69
Gerrit-Change-Number: 732440
Gerrit-PatchSet: 1
Gerrit-Owner: Chris Smith <chris...@google.com>
unsatisfied_requirement
satisfied_requirement
open
diffy
Reply all
Reply to author
Forward
0 new messages