From d3ed898aa8a312e7e3290a8ac5a123149de68011 Mon Sep 17 00:00:00 2001 From: Patrick Jones Date: Wed, 13 Jan 2021 20:38:24 +0000 Subject: [PATCH] google: support url-sourced 3rd party credentials Implements functionality to allow for URL-sourced 3rd party credentials, expanding the functionality added in #462 . Change-Id: Ib7615fb618486612960d60bee6b9a1ecf5de1404 GitHub-Last-Rev: 95713928e495d51d2209bb81cbf2c16185441145 GitHub-Pull-Request: golang/oauth2#466 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/283372 Run-TryBot: Cody Oss TryBot-Result: Go Bot Reviewed-by: Cody Oss Trust: Tyler Bui-Palsulich Trust: Cody Oss --- google/google.go | 33 ++++--- .../externalaccount/basecredentials.go | 6 +- .../externalaccount/filecredsource_test.go | 3 +- .../internal/externalaccount/urlcredsource.go | 71 ++++++++++++++ .../externalaccount/urlcredsource_test.go | 92 +++++++++++++++++++ 5 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 google/internal/externalaccount/urlcredsource.go create mode 100644 google/internal/externalaccount/urlcredsource_test.go diff --git a/google/google.go b/google/google.go index e247491..2c8f1bd 100644 --- a/google/google.go +++ b/google/google.go @@ -115,14 +115,13 @@ type credentialsFile struct { RefreshToken string `json:"refresh_token"` // External Account fields - Audience string `json:"audience"` - SubjectTokenType string `json:"subject_token_type"` - TokenURLExternal string `json:"token_url"` - TokenInfoURL string `json:"token_info_url"` - ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` - CredentialSource externalaccount.CredentialSource `json:"credential_source"` - QuotaProjectID string `json:"quota_project_id"` - + Audience string `json:"audience"` + SubjectTokenType string `json:"subject_token_type"` + TokenURLExternal string `json:"token_url"` + TokenInfoURL string `json:"token_info_url"` + ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"` + CredentialSource externalaccount.CredentialSource `json:"credential_source"` + QuotaProjectID string `json:"quota_project_id"` } func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { @@ -155,16 +154,16 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau return cfg.TokenSource(ctx, tok), nil case externalAccountKey: cfg := &externalaccount.Config{ - Audience: f.Audience, - SubjectTokenType: f.SubjectTokenType, - TokenURL: f.TokenURLExternal, - TokenInfoURL: f.TokenInfoURL, + Audience: f.Audience, + SubjectTokenType: f.SubjectTokenType, + TokenURL: f.TokenURLExternal, + TokenInfoURL: f.TokenInfoURL, ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL, - ClientSecret: f.ClientSecret, - ClientID: f.ClientID, - CredentialSource: f.CredentialSource, - QuotaProjectID: f.QuotaProjectID, - Scopes: scopes, + ClientSecret: f.ClientSecret, + ClientID: f.ClientID, + CredentialSource: f.CredentialSource, + QuotaProjectID: f.QuotaProjectID, + Scopes: scopes, } return cfg.TokenSource(ctx), nil case "": diff --git a/google/internal/externalaccount/basecredentials.go b/google/internal/externalaccount/basecredentials.go index 3291d46..dff0881 100644 --- a/google/internal/externalaccount/basecredentials.go +++ b/google/internal/externalaccount/basecredentials.go @@ -66,9 +66,11 @@ type CredentialSource struct { } // parse determines the type of CredentialSource needed -func (c *Config) parse() baseCredentialSource { +func (c *Config) parse(ctx context.Context) baseCredentialSource { if c.CredentialSource.File != "" { return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format} + } else if c.CredentialSource.URL != "" { + return urlCredentialSource{URL: c.CredentialSource.URL, Format: c.CredentialSource.Format, ctx: ctx} } return nil } @@ -87,7 +89,7 @@ type tokenSource struct { func (ts tokenSource) Token() (*oauth2.Token, error) { conf := ts.conf - credSource := conf.parse() + credSource := conf.parse(ts.ctx) if credSource == nil { return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } diff --git a/google/internal/externalaccount/filecredsource_test.go b/google/internal/externalaccount/filecredsource_test.go index 0bc8048..56dd71e 100644 --- a/google/internal/externalaccount/filecredsource_test.go +++ b/google/internal/externalaccount/filecredsource_test.go @@ -5,6 +5,7 @@ package externalaccount import ( + "context" "testing" ) @@ -55,7 +56,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) { tfc.CredentialSource = test.cs t.Run(test.name, func(t *testing.T) { - out, err := tfc.parse().subjectToken() + out, err := tfc.parse(context.Background()).subjectToken() if err != nil { t.Errorf("Method subjectToken() errored.") } else if test.want != out { diff --git a/google/internal/externalaccount/urlcredsource.go b/google/internal/externalaccount/urlcredsource.go new file mode 100644 index 0000000..b0d5d35 --- /dev/null +++ b/google/internal/externalaccount/urlcredsource.go @@ -0,0 +1,71 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "golang.org/x/oauth2" + "io" + "io/ioutil" + "net/http" +) + +type urlCredentialSource struct { + URL string + Headers map[string]string + Format format + ctx context.Context +} + +func (cs urlCredentialSource) subjectToken() (string, error) { + client := oauth2.NewClient(cs.ctx, nil) + req, err := http.NewRequest("GET", cs.URL, nil) + if err != nil { + return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err) + } + req = req.WithContext(cs.ctx) + + for key, val := range cs.Headers { + req.Header.Add(key, val) + } + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err) + } + defer resp.Body.Close() + + tokenBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err) + } + + switch cs.Format.Type { + case "json": + jsonData := make(map[string]interface{}) + err = json.Unmarshal(tokenBytes, &jsonData) + if err != nil { + return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err) + } + val, ok := jsonData[cs.Format.SubjectTokenFieldName] + if !ok { + return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials") + } + token, ok := val.(string) + if !ok { + return "", errors.New("oauth2/google: improperly formatted subject token") + } + return token, nil + case "text": + return string(tokenBytes), nil + case "": + return string(tokenBytes), nil + default: + return "", errors.New("oauth2/google: invalid credential_source file format type") + } + +} diff --git a/google/internal/externalaccount/urlcredsource_test.go b/google/internal/externalaccount/urlcredsource_test.go new file mode 100644 index 0000000..592610f --- /dev/null +++ b/google/internal/externalaccount/urlcredsource_test.go @@ -0,0 +1,92 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package externalaccount + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +var myURLToken = "testTokenValue" + +func TestRetrieveURLSubjectToken_Text(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + w.Write([]byte("testTokenValue")) + })) + cs := CredentialSource{ + URL: ts.URL, + Format: format{Type: fileTypeText}, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("retrieveSubjectToken() failed: %v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +} + +// Checking that retrieveSubjectToken properly defaults to type text +func TestRetrieveURLSubjectToken_Untyped(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Unexpected request method, %v is found", r.Method) + } + w.Write([]byte("testTokenValue")) + })) + cs := CredentialSource{ + URL: ts.URL, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("Failed to retrieve URL subject token: %v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +} + +func TestRetrieveURLSubjectToken_JSON(t *testing.T) { + type tokenResponse struct { + TestToken string `json:"SubjToken"` + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Method, "GET"; got != want { + t.Errorf("got %v, but want %v", r.Method, want) + } + resp := tokenResponse{TestToken: "testTokenValue"} + jsonResp, err := json.Marshal(resp) + if err != nil { + t.Errorf("Failed to marshal values: %v", err) + } + w.Write(jsonResp) + })) + cs := CredentialSource{ + URL: ts.URL, + Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"}, + } + tfc := testFileConfig + tfc.CredentialSource = cs + + out, err := tfc.parse(context.Background()).subjectToken() + if err != nil { + t.Fatalf("%v", err) + } + if out != myURLToken { + t.Errorf("got %v but want %v", out, myURLToken) + } +}