From 52670883a1d0209cf606ef35487d8a7cf9539fdf Mon Sep 17 00:00:00 2001 From: Adriana Date: Tue, 27 Dec 2022 16:43:46 -0500 Subject: [PATCH 1/5] feat(idtoken): NewTokenSource allows impersonated_service_account creds --- idtoken/idtoken.go | 82 ++++++++++++++++++++++++++++------------- idtoken/idtoken_test.go | 80 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 25 deletions(-) create mode 100644 idtoken/idtoken_test.go diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go index 3dce463bd61..152ac9c2007 100644 --- a/idtoken/idtoken.go +++ b/idtoken/idtoken.go @@ -8,12 +8,16 @@ import ( "context" "encoding/json" "fmt" + "log" "net/http" + "path/filepath" + "strings" "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" "golang.org/x/oauth2/google" + "google.golang.org/api/impersonate" "google.golang.org/api/internal" "google.golang.org/api/option" "google.golang.org/api/option/internaloption" @@ -67,6 +71,7 @@ func NewClient(ctx context.Context, audience string, opts ...ClientOption) (*htt // provided and configured with the supplied options. The parameter audience may // not be empty. func NewTokenSource(ctx context.Context, audience string, opts ...ClientOption) (oauth2.TokenSource, error) { + option.WithScopes() if audience == "" { return nil, fmt.Errorf("idtoken: must supply a non-empty audience") } @@ -103,45 +108,72 @@ func newTokenSource(ctx context.Context, audience string, ds *internal.DialSetti } func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds *internal.DialSettings) (oauth2.TokenSource, error) { - if err := isServiceAccount(data); err != nil { - return nil, err - } - cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...) + allowedType, err := getAllowedType(data) if err != nil { return nil, err } - - customClaims := ds.CustomClaims - if customClaims == nil { - customClaims = make(map[string]interface{}) - } - customClaims["target_audience"] = audience - - cfg.PrivateClaims = customClaims - cfg.UseIDToken = true - - ts := cfg.TokenSource(ctx) - tok, err := ts.Token() - if err != nil { - return nil, err + if allowedType == "service_account" { + cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...) + if err != nil { + return nil, err + } + customClaims := ds.CustomClaims + if customClaims == nil { + customClaims = make(map[string]interface{}) + } + customClaims["target_audience"] = audience + + cfg.PrivateClaims = customClaims + cfg.UseIDToken = true + + ts := cfg.TokenSource(ctx) + tok, err := ts.Token() + if err != nil { + return nil, err + } + return oauth2.ReuseTokenSource(tok, ts), nil + } else { + // if allowedType is "impersonated_service_account": + type url struct { + ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + } + var accountUrl *url + if err := json.Unmarshal(data, &accountUrl); err != nil { + return nil, err + } + account := filepath.Base(accountUrl.ServiceAccountImpersonationURL) + account = strings.Split(account, ":")[0] + + config := impersonate.IDTokenConfig{ + Audience: audience, + TargetPrincipal: account, + IncludeEmail: true, + } + ts, err := impersonate.IDTokenSource(ctx, config) + if err != nil { + log.Println(err) + } + return ts, nil } - return oauth2.ReuseTokenSource(tok, ts), nil } -func isServiceAccount(data []byte) error { +// isOfAllowedType returns the credentials type as a string, and an error. +// allowed types are "service_account" and "impersonated_service_account" +func getAllowedType(data []byte) (string, error) { if len(data) == 0 { - return fmt.Errorf("idtoken: credential provided is 0 bytes") + return "", fmt.Errorf("idtoken: credential provided is 0 bytes") } var f struct { Type string `json:"type"` } + // if not service account return an error if err := json.Unmarshal(data, &f); err != nil { - return err + return "", err } - if f.Type != "service_account" { - return fmt.Errorf("idtoken: credential must be service_account, found %q", f.Type) + if f.Type != "service_account" && f.Type != "impersonated_service_account" { + return "", fmt.Errorf("idtoken: credential must be service_account or impersonated_service_account, found %q", f.Type) } - return nil + return f.Type, nil } // WithCustomClaims optionally specifies custom private claims for an ID token. diff --git a/idtoken/idtoken_test.go b/idtoken/idtoken_test.go new file mode 100644 index 00000000000..367f12d8710 --- /dev/null +++ b/idtoken/idtoken_test.go @@ -0,0 +1,80 @@ +// Copyright 2020 Google LLC. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package idtoken + +import ( + "context" + "reflect" + "testing" + + "golang.org/x/oauth2" + "google.golang.org/api/internal" +) + +var TokenSource oauth2.TokenSource + +func TestNewTokenSource(t *testing.T) { + tests := []struct { + name string + ctx context.Context + audience string + want oauth2.TokenSource + wantErr bool + }{ + { + name: "works", + ctx: context.Background(), + audience: "https://apikeys.googleapis.com", + want: TokenSource, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewTokenSource(tt.ctx, tt.audience) + if (err != nil) != tt.wantErr { + t.Errorf("NewTokenSource() error = %v, wantErr %v", err, tt.wantErr) + return + } + tok, err := got.Token() + if (err != nil) != tt.wantErr { + t.Errorf("NewTokenSource() error = %v, wantErr %v", err, tt.wantErr) + return + } + _, err = Validate(tt.ctx, tok.AccessToken, tt.audience) + if err != nil { + t.Errorf("NewTokenSource() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_newTokenSource(t *testing.T) { + type args struct { + ctx context.Context + audience string + ds *internal.DialSettings + } + tests := []struct { + name string + args args + want oauth2.TokenSource + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newTokenSource(tt.args.ctx, tt.args.audience, tt.args.ds) + if (err != nil) != tt.wantErr { + t.Errorf("newTokenSource() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("newTokenSource() = %v, want %v", got, tt.want) + } + }) + } +} From 79b862a2bf2950a0690cced98f2d38c0e5361af8 Mon Sep 17 00:00:00 2001 From: Adriana Date: Tue, 3 Jan 2023 11:06:04 -0500 Subject: [PATCH 2/5] Address requested changes --- idtoken/idtoken.go | 42 +++++++++++++------ idtoken/idtoken_test.go | 80 ------------------------------------- idtoken/integration_test.go | 3 +- 3 files changed, 32 insertions(+), 93 deletions(-) delete mode 100644 idtoken/idtoken_test.go diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go index 152ac9c2007..2049687b334 100644 --- a/idtoken/idtoken.go +++ b/idtoken/idtoken.go @@ -29,6 +29,14 @@ import ( // ClientOption is for configuring a Google API client or transport. type ClientOption = option.ClientOption +type credentialsType int + +const ( + unknownCredType credentialsType = iota + serviceAccount + impersonatedServiceAccount +) + // NewClient creates a HTTP Client that automatically adds an ID token to each // request via an Authorization header. The token will have the audience // provided and be configured with the supplied options. The parameter audience @@ -71,7 +79,6 @@ func NewClient(ctx context.Context, audience string, opts ...ClientOption) (*htt // provided and configured with the supplied options. The parameter audience may // not be empty. func NewTokenSource(ctx context.Context, audience string, opts ...ClientOption) (oauth2.TokenSource, error) { - option.WithScopes() if audience == "" { return nil, fmt.Errorf("idtoken: must supply a non-empty audience") } @@ -112,7 +119,8 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds if err != nil { return nil, err } - if allowedType == "service_account" { + switch allowedType { + case serviceAccount: cfg, err := google.JWTConfigFromJSON(data, ds.GetScopes()...) if err != nil { return nil, err @@ -132,8 +140,7 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds return nil, err } return oauth2.ReuseTokenSource(tok, ts), nil - } else { - // if allowedType is "impersonated_service_account": + case impersonatedServiceAccount: type url struct { ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` } @@ -154,26 +161,37 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds log.Println(err) } return ts, nil + default: + return nil, fmt.Errorf("idtoken: unsupported credentials type") } } -// isOfAllowedType returns the credentials type as a string, and an error. +// getAllowedType returns the credentials type of type credentialsType, and an error. // allowed types are "service_account" and "impersonated_service_account" -func getAllowedType(data []byte) (string, error) { +func getAllowedType(data []byte) (credentialsType, error) { + var t credentialsType if len(data) == 0 { - return "", fmt.Errorf("idtoken: credential provided is 0 bytes") + return t, fmt.Errorf("idtoken: credential provided is 0 bytes") } var f struct { Type string `json:"type"` } - // if not service account return an error if err := json.Unmarshal(data, &f); err != nil { - return "", err + return t, err } - if f.Type != "service_account" && f.Type != "impersonated_service_account" { - return "", fmt.Errorf("idtoken: credential must be service_account or impersonated_service_account, found %q", f.Type) + t = parseCredType(f.Type) + return t, nil +} + +func parseCredType(typeString string) credentialsType { + switch typeString { + case "service_account": + return serviceAccount + case "impersonated_service_account": + return impersonatedServiceAccount + default: + return unknownCredType } - return f.Type, nil } // WithCustomClaims optionally specifies custom private claims for an ID token. diff --git a/idtoken/idtoken_test.go b/idtoken/idtoken_test.go deleted file mode 100644 index 367f12d8710..00000000000 --- a/idtoken/idtoken_test.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020 Google LLC. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package idtoken - -import ( - "context" - "reflect" - "testing" - - "golang.org/x/oauth2" - "google.golang.org/api/internal" -) - -var TokenSource oauth2.TokenSource - -func TestNewTokenSource(t *testing.T) { - tests := []struct { - name string - ctx context.Context - audience string - want oauth2.TokenSource - wantErr bool - }{ - { - name: "works", - ctx: context.Background(), - audience: "https://apikeys.googleapis.com", - want: TokenSource, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := NewTokenSource(tt.ctx, tt.audience) - if (err != nil) != tt.wantErr { - t.Errorf("NewTokenSource() error = %v, wantErr %v", err, tt.wantErr) - return - } - tok, err := got.Token() - if (err != nil) != tt.wantErr { - t.Errorf("NewTokenSource() error = %v, wantErr %v", err, tt.wantErr) - return - } - _, err = Validate(tt.ctx, tok.AccessToken, tt.audience) - if err != nil { - t.Errorf("NewTokenSource() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_newTokenSource(t *testing.T) { - type args struct { - ctx context.Context - audience string - ds *internal.DialSettings - } - tests := []struct { - name string - args args - want oauth2.TokenSource - wantErr bool - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := newTokenSource(tt.args.ctx, tt.args.audience, tt.args.ds) - if (err != nil) != tt.wantErr { - t.Errorf("newTokenSource() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("newTokenSource() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/idtoken/integration_test.go b/idtoken/integration_test.go index 32281fa8507..93f42f55a92 100644 --- a/idtoken/integration_test.go +++ b/idtoken/integration_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Google LLC. +// Copyright 2023 Google LLC. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -6,6 +6,7 @@ package idtoken_test import ( "context" + "encoding/json" "net/http" "os" "strings" From 1b86a8fa992ee8d19c5b0e623978d5d7817f637f Mon Sep 17 00:00:00 2001 From: Adriana Date: Tue, 3 Jan 2023 16:45:09 -0500 Subject: [PATCH 3/5] reformat --- idtoken/integration_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/idtoken/integration_test.go b/idtoken/integration_test.go index 93f42f55a92..32281fa8507 100644 --- a/idtoken/integration_test.go +++ b/idtoken/integration_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google LLC. +// Copyright 2020 Google LLC. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -6,7 +6,6 @@ package idtoken_test import ( "context" - "encoding/json" "net/http" "os" "strings" From 8c8c04818523510695dc5034398e62a86776e754 Mon Sep 17 00:00:00 2001 From: Adriana Date: Tue, 3 Jan 2023 16:47:59 -0500 Subject: [PATCH 4/5] format again --- idtoken/idtoken.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go index 2049687b334..3a486952fa2 100644 --- a/idtoken/idtoken.go +++ b/idtoken/idtoken.go @@ -8,7 +8,6 @@ import ( "context" "encoding/json" "fmt" - "log" "net/http" "path/filepath" "strings" @@ -158,7 +157,7 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds } ts, err := impersonate.IDTokenSource(ctx, config) if err != nil { - log.Println(err) + return nil, err } return ts, nil default: From 7bda6ada1050e94aa2d3102a43f565c58c353b31 Mon Sep 17 00:00:00 2001 From: Adriana Date: Tue, 3 Jan 2023 17:02:48 -0500 Subject: [PATCH 5/5] formatting --- idtoken/idtoken.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go index 3a486952fa2..b7a82e92bf0 100644 --- a/idtoken/idtoken.go +++ b/idtoken/idtoken.go @@ -143,11 +143,11 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds type url struct { ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` } - var accountUrl *url - if err := json.Unmarshal(data, &accountUrl); err != nil { + var accountURL *url + if err := json.Unmarshal(data, &accountURL); err != nil { return nil, err } - account := filepath.Base(accountUrl.ServiceAccountImpersonationURL) + account := filepath.Base(accountURL.ServiceAccountImpersonationURL) account = strings.Split(account, ":")[0] config := impersonate.IDTokenConfig{