From d1c9d1065f07e5ca2d0f14b478f4253c8e30e929 Mon Sep 17 00:00:00 2001 From: Joseph Stevens Date: Sat, 12 Oct 2024 16:45:18 -0700 Subject: [PATCH] enable standard forms of GCP auth for oci sources Signed-off-by: Joseph Stevens --- oci/auth/gcp/auth.go | 88 +++++++++-------------- oci/auth/gcp/auth_test.go | 134 ++++++++++++++++++----------------- oci/auth/login/login_test.go | 97 +++++++++++++++++-------- oci/go.mod | 3 +- oci/go.sum | 6 +- oci/tests/integration/go.mod | 3 +- oci/tests/integration/go.sum | 6 +- 7 files changed, 181 insertions(+), 156 deletions(-) diff --git a/oci/auth/gcp/auth.go b/oci/auth/gcp/auth.go index 584f0b25..3481048b 100644 --- a/oci/auth/gcp/auth.go +++ b/oci/auth/gcp/auth.go @@ -18,10 +18,7 @@ package gcp import ( "context" - "encoding/json" "fmt" - "io" - "net/http" "net/url" "strings" "time" @@ -31,17 +28,10 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/fluxcd/pkg/oci" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" ) -type gceToken struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` -} - -// GCP_TOKEN_URL is the default GCP metadata endpoint used for authentication. -const GCP_TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" - // ValidHost returns if a given host is a valid GCR host. func ValidHost(host string) bool { return host == "gcr.io" || strings.HasSuffix(host, ".gcr.io") || strings.HasSuffix(host, "-docker.pkg.dev") @@ -50,8 +40,8 @@ func ValidHost(host string) bool { // Client is a GCP GCR client which can log into the registry and return // authorization information. type Client struct { - tokenURL string - proxyURL *url.URL + proxyURL *url.URL + tokenSource oauth2.TokenSource } // Option is a functional option for configuring the client. @@ -64,69 +54,57 @@ func WithProxyURL(proxyURL *url.URL) Option { } } +// WithTokenSource sets a custom token source for the client. +func (c *Client) WithTokenSource(ts oauth2.TokenSource) *Client { + c.tokenSource = ts + return c +} + // NewClient creates a new GCR client with default configurations. func NewClient(opts ...Option) *Client { - client := &Client{tokenURL: GCP_TOKEN_URL} + client := &Client{} for _, opt := range opts { opt(client) } return client } -// WithTokenURL sets the token URL used by the GCR client. -func (c *Client) WithTokenURL(url string) *Client { - c.tokenURL = url - return c -} - -// getLoginAuth obtains authentication by getting a token from the metadata API -// on GCP. This assumes that the pod has right to pull the image which would be -// the case if it is hosted on GCP. It works with both service account and -// workload identity enabled clusters. +// getLoginAuth obtains authentication using the default GCP credential chain. +// This supports various authentication methods including service account JSON, +// external account JSON, user credentials, and GCE metadata service. func (c *Client) getLoginAuth(ctx context.Context) (authn.AuthConfig, time.Time, error) { var authConfig authn.AuthConfig - request, err := http.NewRequestWithContext(ctx, http.MethodGet, c.tokenURL, nil) - if err != nil { - return authConfig, time.Time{}, err - } + // Define the required scopes for accessing GCR. + scopes := []string{"https://www.googleapis.com/auth/cloud-platform"} - request.Header.Add("Metadata-Flavor", "Google") + var tokenSource oauth2.TokenSource + var err error - var transport http.RoundTripper - if c.proxyURL != nil { - t := http.DefaultTransport.(*http.Transport).Clone() - t.Proxy = http.ProxyURL(c.proxyURL) - transport = t + // Use the injected token source if available; otherwise, use the default. + if c.tokenSource != nil { + tokenSource = c.tokenSource + } else { + // Obtain the default token source. + tokenSource, err = google.DefaultTokenSource(ctx, scopes...) + if err != nil { + return authConfig, time.Time{}, fmt.Errorf("failed to get default token source: %w", err) + } } - client := &http.Client{Transport: transport} - response, err := client.Do(request) + // Retrieve the token. + token, err := tokenSource.Token() if err != nil { - return authConfig, time.Time{}, err - } - defer response.Body.Close() - defer io.Copy(io.Discard, response.Body) - - if response.StatusCode != http.StatusOK { - return authConfig, time.Time{}, fmt.Errorf("unexpected status from metadata service: %s", response.Status) - } - - var accessToken gceToken - decoder := json.NewDecoder(response.Body) - if err := decoder.Decode(&accessToken); err != nil { - return authConfig, time.Time{}, err + return authConfig, time.Time{}, fmt.Errorf("failed to obtain token: %w", err) } + // Set up the authentication configuration. authConfig = authn.AuthConfig{ Username: "oauth2accesstoken", - Password: accessToken.AccessToken, + Password: token.AccessToken, } - // add expiresIn seconds to the current time to get the expiry time - expiresAt := time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second) - - return authConfig, expiresAt, nil + return authConfig, token.Expiry, nil } // Login attempts to get the authentication material for GCR. diff --git a/oci/auth/gcp/auth_test.go b/oci/auth/gcp/auth_test.go index 185c34f6..31aba6fd 100644 --- a/oci/auth/gcp/auth_test.go +++ b/oci/auth/gcp/auth_test.go @@ -18,49 +18,51 @@ package gcp import ( "context" - "net/http" - "net/http/httptest" + "fmt" "testing" "time" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" . "github.com/onsi/gomega" + "golang.org/x/oauth2" ) const testValidGCRImage = "gcr.io/foo/bar:v1" +type fakeTokenSource struct { + token *oauth2.Token + err error +} + +func (f *fakeTokenSource) Token() (*oauth2.Token, error) { + return f.token, f.err +} + func TestGetLoginAuth(t *testing.T) { tests := []struct { name string - responseBody string - statusCode int + token *oauth2.Token + tokenErr error wantErr bool wantAuthConfig authn.AuthConfig }{ { name: "success", - responseBody: `{ - "access_token": "some-token", - "expires_in": 10, - "token_type": "foo" -}`, - statusCode: http.StatusOK, + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, wantAuthConfig: authn.AuthConfig{ Username: "oauth2accesstoken", Password: "some-token", }, }, { - name: "fail", - statusCode: http.StatusInternalServerError, - wantErr: true, - }, - { - name: "invalid response", - responseBody: "foo", - statusCode: http.StatusOK, - wantErr: true, + name: "fail", + tokenErr: fmt.Errorf("token error"), + wantErr: true, }, } @@ -68,22 +70,17 @@ func TestGetLoginAuth(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(tt.responseBody)) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: tt.token, + err: tt.tokenErr, } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - gc := NewClient().WithTokenURL(srv.URL) + gc := NewClient().WithTokenSource(fakeTS) a, expiresAt, err := gc.getLoginAuth(context.TODO()) g.Expect(err != nil).To(Equal(tt.wantErr)) if !tt.wantErr { - g.Expect(expiresAt).To(BeTemporally("~", time.Now().Add(10*time.Second), time.Second)) - } - if tt.statusCode == http.StatusOK { + g.Expect(expiresAt).To(BeTemporally("~", tt.token.Expiry, time.Second)) g.Expect(a).To(Equal(tt.wantAuthConfig)) } }) @@ -111,40 +108,48 @@ func TestValidHost(t *testing.T) { func TestLogin(t *testing.T) { tests := []struct { - name string - autoLogin bool - image string - statusCode int - testOIDC bool - wantErr bool + name string + autoLogin bool + image string + token *oauth2.Token + tokenErr error + testOIDC bool + wantErr bool }{ { - name: "no auto login", - autoLogin: false, - image: testValidGCRImage, - statusCode: http.StatusOK, - wantErr: true, + name: "no auto login", + autoLogin: false, + image: testValidGCRImage, + wantErr: true, }, { - name: "with auto login", - autoLogin: true, - image: testValidGCRImage, - testOIDC: true, - statusCode: http.StatusOK, + name: "with auto login", + autoLogin: true, + image: testValidGCRImage, + testOIDC: true, + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, }, { - name: "login failure", - autoLogin: true, - image: testValidGCRImage, - statusCode: http.StatusInternalServerError, - testOIDC: true, - wantErr: true, + name: "login failure", + autoLogin: true, + image: testValidGCRImage, + tokenErr: fmt.Errorf("token error"), + testOIDC: true, + wantErr: true, }, { - name: "non GCR image", - autoLogin: true, - image: "foo/bar:v1", - statusCode: http.StatusOK, + name: "non GCR image", + autoLogin: true, + image: "foo/bar:v1", + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, }, } @@ -152,19 +157,16 @@ func TestLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tt.statusCode) - w.Write([]byte(`{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`)) - } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) - ref, err := name.ParseReference(tt.image) g.Expect(err).ToNot(HaveOccurred()) - gc := NewClient().WithTokenURL(srv.URL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: tt.token, + err: tt.tokenErr, + } + + gc := NewClient().WithTokenSource(fakeTS) _, err = gc.Login(context.TODO(), tt.autoLogin, tt.image, ref) g.Expect(err != nil).To(Equal(tt.wantErr)) diff --git a/oci/auth/login/login_test.go b/oci/auth/login/login_test.go index 2aebdfab..dde4c600 100644 --- a/oci/auth/login/login_test.go +++ b/oci/auth/login/login_test.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" . "github.com/onsi/gomega" + "golang.org/x/oauth2" "github.com/fluxcd/pkg/cache" "github.com/fluxcd/pkg/oci" @@ -38,6 +39,15 @@ import ( "github.com/fluxcd/pkg/oci/auth/gcp" ) +type fakeTokenSource struct { + token *oauth2.Token + err error +} + +func (f *fakeTokenSource) Token() (*oauth2.Token, error) { + return f.token, f.err +} + func TestImageRegistryProvider(t *testing.T) { tests := []struct { name string @@ -100,11 +110,18 @@ func TestLogin(t *testing.T) { }, { name: "gcr", - responseBody: `{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`, providerOpts: ProviderOptions{GcpAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create GCR client and configure the manager. - gcrClient := gcp.NewClient().WithTokenURL(serverURL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: time.Now().Add(10 * time.Second), + }, + } + // Create GCR client with fake token source + gcrClient := gcp.NewClient().WithTokenSource(fakeTS) mgr.WithGCRClient(gcrClient) *image = "gcr.io/foo/bar:v1" @@ -122,8 +139,8 @@ func TestLogin(t *testing.T) { }, // NOTE: This fails because the azure exchanger uses the image host // to exchange token which can't be modified here without - // interfering image name based categorization of the login - // provider, that's actually being tested here. This is tested in + // interfering with image name based categorization of the login + // provider that's actually being tested here. This is tested in // detail in the azure package. wantErr: true, }, @@ -140,21 +157,28 @@ func TestLogin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - // Create test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.responseBody)) + // Create test server if responseBody is set. + var srv *httptest.Server + if tt.responseBody != "" { + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.responseBody)) + } + srv = httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) mgr := NewManager() var image string if tt.beforeFunc != nil { - tt.beforeFunc(srv.URL, mgr, &image) + serverURL := "" + if srv != nil { + serverURL = srv.URL + } + tt.beforeFunc(serverURL, mgr, &image) } ref, err := name.ParseReference(image) @@ -167,7 +191,7 @@ func TestLogin(t *testing.T) { } func TestLogin_WithCache(t *testing.T) { - timestamp := time.Now().Add(10 * time.Second).Unix() + timestamp := time.Now().Add(10 * time.Second) tests := []struct { name string responseBody string @@ -178,7 +202,7 @@ func TestLogin_WithCache(t *testing.T) { }{ { name: "ecr", - responseBody: fmt.Sprintf(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ=","expiresAt": %d}]}`, timestamp), + responseBody: fmt.Sprintf(`{"authorizationData": [{"authorizationToken": "c29tZS1rZXk6c29tZS1zZWNyZXQ=","expiresAt": %d}]}`, timestamp.Unix()), providerOpts: ProviderOptions{AwsAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { // Create ECR client and configure the manager. @@ -198,11 +222,18 @@ func TestLogin_WithCache(t *testing.T) { }, { name: "gcr", - responseBody: `{"access_token": "some-token","expires_in": 10, "token_type": "foo"}`, providerOpts: ProviderOptions{GcpAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { - // Create GCR client and configure the manager. - gcrClient := gcp.NewClient().WithTokenURL(serverURL) + // Create fake token source + fakeTS := &fakeTokenSource{ + token: &oauth2.Token{ + AccessToken: "some-token", + TokenType: "Bearer", + Expiry: timestamp, + }, + } + // Create GCR client with fake token source + gcrClient := gcp.NewClient().WithTokenSource(fakeTS) mgr.WithGCRClient(gcrClient) *image = "gcr.io/foo/bar:v1" @@ -213,6 +244,7 @@ func TestLogin_WithCache(t *testing.T) { responseBody: `{"refresh_token": "bbbbb"}`, providerOpts: ProviderOptions{AzureAutoLogin: true}, beforeFunc: func(serverURL string, mgr *Manager, image *string) { + // Create ACR client and configure the manager. acrClient := azure.NewClient().WithTokenCredential(&azure.FakeTokenCredential{Token: "foo"}).WithScheme("http") mgr.WithACRClient(acrClient) @@ -231,21 +263,28 @@ func TestLogin_WithCache(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - // Create test server. - handler := func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte(tt.responseBody)) + // Create test server if responseBody is set. + var srv *httptest.Server + if tt.responseBody != "" { + handler := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(tt.responseBody)) + } + srv = httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(func() { + srv.Close() + }) } - srv := httptest.NewServer(http.HandlerFunc(handler)) - t.Cleanup(func() { - srv.Close() - }) mgr := NewManager() var image string if tt.beforeFunc != nil { - tt.beforeFunc(srv.URL, mgr, &image) + serverURL := "" + if srv != nil { + serverURL = srv.URL + } + tt.beforeFunc(serverURL, mgr, &image) } ref, err := name.ParseReference(image) @@ -272,7 +311,7 @@ func TestLogin_WithCache(t *testing.T) { expiration, err := cache.GetExpiration(obj) g.Expect(err).ToNot(HaveOccurred()) g.Expect(expiration).ToNot(BeZero()) - g.Expect(expiration).To(BeTemporally("~", time.Unix(timestamp, 0), 1*time.Second)) + g.Expect(expiration).To(BeTemporally("~", timestamp, 1*time.Second)) } }) } diff --git a/oci/go.mod b/oci/go.mod index 8cc1706f..68c2da10 100644 --- a/oci/go.mod +++ b/oci/go.mod @@ -26,9 +26,11 @@ require ( github.com/google/go-containerregistry v0.20.2 github.com/onsi/gomega v1.34.2 github.com/sirupsen/logrus v1.9.3 + golang.org/x/oauth2 v0.23.0 ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect @@ -136,7 +138,6 @@ require ( go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.29.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/term v0.24.0 // indirect diff --git a/oci/go.sum b/oci/go.sum index 7ac23d2f..79bf0a28 100644 --- a/oci/go.sum +++ b/oci/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= @@ -406,8 +408,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/oci/tests/integration/go.mod b/oci/tests/integration/go.mod index 659ff987..84342771 100644 --- a/oci/tests/integration/go.mod +++ b/oci/tests/integration/go.mod @@ -30,6 +30,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect dario.cat/mergo v1.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect @@ -131,7 +132,7 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.29.0 // indirect - golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/term v0.24.0 // indirect diff --git a/oci/tests/integration/go.sum b/oci/tests/integration/go.sum index b5263679..3df784a7 100644 --- a/oci/tests/integration/go.sum +++ b/oci/tests/integration/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= @@ -368,8 +370,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=