Skip to content

Commit acb5230

Browse files
committed
feat(option): Deprecate unsafe credentials JSON loading options
Add safer credentials JSON loading options. Add option.WithAuthCredentialsFile and option.WithAuthCredentialsJSON 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. Deprecate the less safe option.WithCredentialsFile and option.WithCredentialsJSON functions.
1 parent 0bcacee commit acb5230

File tree

16 files changed

+468
-54
lines changed

16 files changed

+468
-54
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,22 @@ client, err := sheets.NewService(ctx)
6060
```
6161

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

6666
```go
67-
client, err := sheets.NewService(ctx, option.WithCredentialsFile("path/to/keyfile.json"))
67+
client, err := sheets.NewService(ctx, option.WithAuthCredentialsFile(option.ServiceAccount, "path/to/keyfile.json"))
6868
```
6969

70+
Similarly, you can use JSON credentials directly with [`option.WithAuthCredentialsJSON`](https://pkg.go.dev/google.golang.org/api/option#WithAuthCredentialsJSON):
71+
72+
```go
73+
// where jsonKey is a []byte containing the JSON key
74+
client, err := sheets.NewService(ctx, option.WithAuthCredentialsJSON(option.ServiceAccount, jsonKey))
75+
```
76+
77+
The older `option.WithCredentialsFile` and `option.WithCredentialsJSON` functions are deprecated due to a potential security risk.
78+
7079
You can exert more control over authorization by using the [`golang.org/x/oauth2`](https://pkg.go.dev/golang.org/x/oauth2)
7180
package to create an `oauth2.TokenSource`. Then pass [`option.WithTokenSource`](https://pkg.go.dev/google.golang.org/api/option#WithTokenSource)
7281
to the `NewService` function:

idtoken/idtoken.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,13 @@ func newTokenSourceNewAuth(ctx context.Context, audience string, ds *internal.Di
110110
if ds.AuthCredentials != nil {
111111
return nil, fmt.Errorf("idtoken: option.WithTokenProvider not supported")
112112
}
113+
credsJSON, _ := ds.GetAuthCredentialsJSON()
114+
credsFile, _ := ds.GetAuthCredentialsFile()
113115
creds, err := newidtoken.NewCredentials(&newidtoken.Options{
114116
Audience: audience,
115117
CustomClaims: ds.CustomClaims,
116-
CredentialsFile: ds.CredentialsFile,
117-
CredentialsJSON: ds.CredentialsJSON,
118+
CredentialsFile: credsFile,
119+
CredentialsJSON: credsJSON,
118120
Client: oauth2.NewClient(ctx, nil),
119121
Logger: ds.Logger,
120122
})
@@ -233,20 +235,159 @@ func (w withCustomClaims) Apply(o *internal.DialSettings) {
233235
o.CustomClaims = w
234236
}
235237

238+
// CredentialsType specifies the type of JSON credentials being provided
239+
// to a loading function such as [WithAuthCredentialsFile] or
240+
// [WithAuthCredentialsJSON].
241+
type CredentialsType = option.CredentialsType
242+
243+
const (
244+
// Unknown represents an unknown JSON file type.
245+
//
246+
// IMPORTANT:
247+
// This credential type does not validate the credential configuration. A security
248+
// risk occurs when a credential configuration configured with malicious urls
249+
// is used.
250+
// You should validate credential configurations provided by untrusted sources.
251+
// See [Security requirements when using credential configurations from an external
252+
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
253+
// for more details.
254+
Unknown = option.Unknown
255+
// ServiceAccount represents a service account file type.
256+
ServiceAccount = option.ServiceAccount
257+
// User represents a user credentials file type.
258+
User = option.User
259+
// ImpersonatedServiceAccount represents an impersonated service account file type.
260+
//
261+
// IMPORTANT:
262+
// This credential type does not validate the credential configuration. A security
263+
// risk occurs when a credential configuration configured with malicious urls
264+
// is used.
265+
// You should validate credential configurations provided by untrusted sources.
266+
// See [Security requirements when using credential configurations from an external
267+
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
268+
// for more details.
269+
ImpersonatedServiceAccount = option.ImpersonatedServiceAccount
270+
// ExternalAccount represents an external account file type.
271+
//
272+
// IMPORTANT:
273+
// This credential type does not validate the credential configuration. A security
274+
// risk occurs when a credential configuration configured with malicious urls
275+
// is used.
276+
// You should validate credential configurations provided by untrusted sources.
277+
// See [Security requirements when using credential configurations from an external
278+
// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
279+
// for more details.
280+
ExternalAccount = option.ExternalAccount
281+
)
282+
236283
// WithCredentialsFile returns a ClientOption that authenticates
237284
// API calls with the given service account or refresh token JSON
238285
// credentials file.
286+
//
287+
// Important: If you accept a credential configuration (credential
288+
// JSON/File/Stream) from an external source for authentication to Google
289+
// Cloud Platform, you must validate it before providing it to any Google
290+
// API or library. Providing an unvalidated credential configuration to
291+
// Google APIs can compromise the security of your systems and data. For
292+
// more information, refer to [Validate credential configurations from
293+
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
294+
//
295+
// Deprecated: This function is being deprecated because of a potential security risk.
296+
//
297+
// This function does not validate the credential configuration. The security
298+
// risk occurs when a credential configuration is accepted from a source that
299+
// is not under your control and used without validation on your side.
300+
//
301+
// If you know that you will be loading credential configurations of a
302+
// specific type, it is recommended to use a credential-type-specific
303+
// option function.
304+
// This will ensure that an unexpected credential type with potential for
305+
// malicious intent is not loaded unintentionally. You might still have to do
306+
// validation for certain credential types. Please follow the recommendation
307+
// for that function. For example, if you want to load only service accounts,
308+
// you can use [WithAuthCredentialsFile] with [ServiceAccount]:
309+
// ```
310+
// option.WithAuthCredentialsFile(option.ServiceAccount, "/path/to/file.json")
311+
// ```
312+
//
313+
// If you are loading your credential configuration from an untrusted source and have
314+
// not mitigated the risks (e.g. by validating the configuration yourself), make
315+
// these changes as soon as possible to prevent security risks to your environment.
316+
//
317+
// Regardless of the function used, it is always your responsibility to validate
318+
// configurations received from external sources.
239319
func WithCredentialsFile(filename string) ClientOption {
240320
return option.WithCredentialsFile(filename)
241321
}
242322

323+
// WithAuthCredentialsFile returns a ClientOption that authenticates API calls
324+
// with the given JSON credentials file and credential type.
325+
//
326+
// Important: If you accept a credential configuration (credential
327+
// JSON/File/Stream) from an external source for authentication to Google
328+
// Cloud Platform, you must validate it before providing it to any Google
329+
// API or library. Providing an unvalidated credential configuration to
330+
// Google APIs can compromise the security of your systems and data. For
331+
// more information, refer to [Validate credential configurations from
332+
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
333+
func WithAuthCredentialsFile(credType CredentialsType, filename string) ClientOption {
334+
return option.WithAuthCredentialsFile(credType, filename)
335+
}
336+
243337
// WithCredentialsJSON returns a ClientOption that authenticates
244338
// API calls with the given service account or refresh token JSON
245339
// credentials.
340+
//
341+
// Important: If you accept a credential configuration (credential
342+
// JSON/File/Stream) from an external source for authentication to Google
343+
// Cloud Platform, you must validate it before providing it to any Google
344+
// API or library. Providing an unvalidated credential configuration to
345+
// Google APIs can compromise the security of your systems and data. For
346+
// more information, refer to [Validate credential configurations from
347+
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
348+
//
349+
// Deprecated: This function is being deprecated because of a potential security risk.
350+
//
351+
// This function does not validate the credential configuration. The security
352+
// risk occurs when a credential configuration is accepted from a source that
353+
// is not under your control and used without validation on your side.
354+
//
355+
// If you know that you will be loading credential configurations of a
356+
// specific type, it is recommended to use a credential-type-specific
357+
// option function.
358+
// This will ensure that an unexpected credential type with potential for
359+
// malicious intent is not loaded unintentionally. You might still have to do
360+
// validation for certain credential types. Please follow the recommendation
361+
// for that function. For example, if you want to load only service accounts,
362+
// you can use [WithAuthCredentialsJSON] with [ServiceAccount]:
363+
// ```
364+
// option.WithAuthCredentialsJSON(option.ServiceAccount, json)
365+
// ```
366+
//
367+
// If you are loading your credential configuration from an untrusted source and have
368+
// not mitigated the risks (e.g. by validating the configuration yourself), make
369+
// these changes as soon as possible to prevent security risks to your environment.
370+
//
371+
// Regardless of the function used, it is always your responsibility to validate
372+
// configurations received from external sources.
246373
func WithCredentialsJSON(p []byte) ClientOption {
247374
return option.WithCredentialsJSON(p)
248375
}
249376

377+
// WithAuthCredentialsJSON returns a ClientOption that authenticates API calls
378+
// with the given JSON credentials and credential type.
379+
//
380+
// Important: If you accept a credential configuration (credential
381+
// JSON/File/Stream) from an external source for authentication to Google
382+
// Cloud Platform, you must validate it before providing it to any Google
383+
// API or library. Providing an unvalidated credential configuration to
384+
// Google APIs can compromise the security of your systems and data. For
385+
// more information, refer to [Validate credential configurations from
386+
// external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
387+
func WithAuthCredentialsJSON(credType CredentialsType, json []byte) ClientOption {
388+
return option.WithAuthCredentialsJSON(credType, json)
389+
}
390+
250391
// WithHTTPClient returns a ClientOption that specifies the HTTP client to use
251392
// as the basis of communications. This option may only be used with services
252393
// that support HTTP as their communication transport. When used, the

impersonate/integration_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func TestCredentialsTokenSourceIntegration(t *testing.T) {
8787
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
8888
Delegates: tt.delegates,
8989
},
90-
option.WithCredentialsFile(tt.baseKeyFile),
90+
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
9191
)
9292
if err != nil {
9393
t.Fatalf("failed to create ts: %v", err)
@@ -143,7 +143,7 @@ func TestIDTokenSourceIntegration(t *testing.T) {
143143
Delegates: tt.delegates,
144144
IncludeEmail: true,
145145
},
146-
option.WithCredentialsFile(tt.baseKeyFile),
146+
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseKeyFile),
147147
)
148148
if err != nil {
149149
t.Fatalf("failed to create ts: %v", err)
@@ -198,7 +198,7 @@ func TestTokenSourceIntegration_user(t *testing.T) {
198198
Scopes: []string{"https://www.googleapis.com/auth/admin.directory.user", "https://www.googleapis.com/auth/admin.directory.group"},
199199
Subject: domainAdmin,
200200
},
201-
option.WithCredentialsFile(baseKeyFile),
201+
option.WithAuthCredentialsFile(option.ServiceAccount, baseKeyFile),
202202
)
203203
if err != nil {
204204
t.Fatalf("failed to create ts: %v", err)

integration-tests/byoid/integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ func getClientID(keyFileName string) (string, error) {
129129
}
130130

131131
func generateGoogleToken(keyFileName string) (string, error) {
132-
ts, err := idtoken.NewTokenSource(context.Background(), oidcAudience, option.WithCredentialsFile(keyFileName))
132+
ts, err := idtoken.NewTokenSource(context.Background(), oidcAudience, option.WithAuthCredentialsFile(option.ServiceAccount, keyFileName))
133133
if err != nil {
134134
return "", nil
135135
}
@@ -175,7 +175,7 @@ func testBYOID(t *testing.T, c config) {
175175
writeConfig(t, c, func(name string) {
176176
// Once the default credentials are obtained,
177177
// we should be able to access Google Cloud resources.
178-
dnsService, err := dns.NewService(context.Background(), option.WithCredentialsFile(name))
178+
dnsService, err := dns.NewService(context.Background(), option.WithAuthCredentialsFile(option.ExternalAccount, name))
179179
if err != nil {
180180
t.Fatalf("Could not establish DNS Service: %v", err)
181181
}

integration-tests/downscope/downscope_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestMain(m *testing.M) {
4646
credentialFileName := os.Getenv(envServiceAccountFile)
4747

4848
var err error
49-
rootCredential, err = transport.Creds(ctx, option.WithCredentialsFile(credentialFileName), option.WithScopes(rootTokenScope))
49+
rootCredential, err = transport.Creds(ctx, option.WithAuthCredentialsFile(option.ServiceAccount, credentialFileName), option.WithScopes(rootTokenScope))
5050

5151
if err != nil {
5252
log.Fatalf("failed to construct root credential: %v", err)

integration-tests/impersonate/impersonate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func TestImpersonatedCredentials(t *testing.T) {
7171
for _, tt := range tests {
7272
t.Run(tt.name, func(t *testing.T) {
7373
svc, err := storage.NewService(ctx,
74-
option.WithCredentialsFile(tt.baseSALocation),
74+
option.WithAuthCredentialsFile(option.ServiceAccount, tt.baseSALocation),
7575
option.ImpersonateCredentials(writerSA, tt.delgates...),
7676
)
7777
if err != nil {

internal/creds.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,13 @@ func detectDefaultFromDialSettings(settings *DialSettings) (*auth.Credentials, e
139139
aud = settings.DefaultAudience
140140
}
141141

142+
credsFile, _ := settings.GetAuthCredentialsFile()
143+
credsJSON, _ := settings.GetAuthCredentialsJSON()
142144
return credentials.DetectDefault(&credentials.DetectOptions{
143145
Scopes: scopes,
144146
Audience: aud,
145-
CredentialsFile: settings.CredentialsFile,
146-
CredentialsJSON: settings.CredentialsJSON,
147+
CredentialsFile: credsFile,
148+
CredentialsJSON: credsJSON,
147149
UseSelfSignedJWT: useSelfSignedJWT,
148150
Logger: settings.Logger,
149151
})
@@ -159,8 +161,8 @@ func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, erro
159161
if len(ds.CredentialsJSON) > 0 {
160162
return credentialsFromJSON(ctx, ds.CredentialsJSON, ds)
161163
}
162-
if ds.CredentialsFile != "" {
163-
data, err := os.ReadFile(ds.CredentialsFile)
164+
if cf, _ := ds.GetAuthCredentialsFile(); cf != "" {
165+
data, err := os.ReadFile(cf)
164166
if err != nil {
165167
return nil, fmt.Errorf("cannot read credentials file: %v", err)
166168
}

internal/creds_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ func TestIsSelfSignedJWTFlow(t *testing.T) {
593593
{
594594
name: "EnableJwtWithScope true",
595595
ds: &DialSettings{
596-
CredentialsFile: "testdata/service-account.json",
596+
AuthCredentialsFile: "testdata/service-account.json",
597597
Scopes: []string{"foo"},
598598
EnableJwtWithScope: true,
599599
},
@@ -602,7 +602,7 @@ func TestIsSelfSignedJWTFlow(t *testing.T) {
602602
{
603603
name: "EnableJwtWithScope false",
604604
ds: &DialSettings{
605-
CredentialsFile: "testdata/service-account.json",
605+
AuthCredentialsFile: "testdata/service-account.json",
606606
Scopes: []string{"foo"},
607607
EnableJwtWithScope: false,
608608
},
@@ -611,7 +611,7 @@ func TestIsSelfSignedJWTFlow(t *testing.T) {
611611
{
612612
name: "UniverseDomain",
613613
ds: &DialSettings{
614-
CredentialsFile: "testdata/service-account.json",
614+
AuthCredentialsFile: "testdata/service-account.json",
615615
Scopes: []string{"foo"},
616616
EnableJwtWithScope: false,
617617
UniverseDomain: "example.com",
@@ -621,7 +621,7 @@ func TestIsSelfSignedJWTFlow(t *testing.T) {
621621
{
622622
name: "UniverseDomainUserAccount",
623623
ds: &DialSettings{
624-
CredentialsFile: "testdata/user-account.json",
624+
AuthCredentialsFile: "testdata/user-account.json",
625625
Scopes: []string{"foo"},
626626
EnableJwtWithScope: false,
627627
UniverseDomain: "example.com",
@@ -632,7 +632,7 @@ func TestIsSelfSignedJWTFlow(t *testing.T) {
632632

633633
for _, tc := range tests {
634634

635-
bytes, err := os.ReadFile(tc.ds.CredentialsFile)
635+
bytes, err := os.ReadFile(tc.ds.AuthCredentialsFile)
636636
if err != nil {
637637
t.Fatal(err)
638638
}

0 commit comments

Comments
 (0)