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
Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/732440
Reviewed-by: Seth Hollyman <shollyman@google.com>
Reviewed-by: Cody Oss <codyoss@google.com>
Reviewed-by: Sai Sunder Srinivasan <saisunder@google.com>
TryBot-Bypass: Cody Oss <codyoss@google.com>
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