From ceaa866219bd09735d89c4e66e48b90534c2ef8f Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Tue, 5 May 2020 16:55:31 -0700 Subject: [PATCH 01/15] google: Add support for 3-legged-OAuth using OAuth Client ID Add OAuthClientTokenSource in google/google.go Add DefaultAuthorizationHandler in authhandler.go --- google/authhandler.go | 23 +++++++++++++++++++++++ google/google.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 google/authhandler.go diff --git a/google/authhandler.go b/google/authhandler.go new file mode 100644 index 000000000..c92bac83c --- /dev/null +++ b/google/authhandler.go @@ -0,0 +1,23 @@ +// 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 google + +import ( + "fmt" +) + +const DefaultState = "state" + +// DefaultAuthorizationHandler is a commandline-based auth handler +// that prints the auth URL on the console and prompts the user to +// authorize in the browser and paste the auth code back via stdin. +// When using this auth handler, DefaultState must be used. +func DefaultAuthorizationHandler(authCodeUrl string) (string, string, error) { + fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeUrl) + fmt.Println("Enter verification code: ") + var code string + fmt.Scanln(&code) + return code, DefaultState, nil +} diff --git a/google/google.go b/google/google.go index 81de32b36..ebcabe5bf 100644 --- a/google/google.go +++ b/google/google.go @@ -207,3 +207,38 @@ func (cs computeSource) Token() (*oauth2.Token, error) { "oauth2.google.serviceAccount": acct, }), nil } + +// AuthorizationHandler is a 3-legged-OAuth helper that +// prompts the user for OAuth consent at the specified Auth URL +// and returns an auth code and state upon approval. +type AuthorizationHandler func(string) (string, string, error) + +// OAuthClientTokenSource returns an oauth2.TokenSource that fetches access tokens +// using 3-legged-OAuth workflow. +// The provided oauth2.Config should be a full configuration containing AuthURL, +// TokenURL, and scope. +// An environment-specific AuthorizationHandler is used to obtain user consent. +// Per OAuth protocol, a unique "state" string should be sent and verified +// before token exchange to prevent CSRF attacks. +func OAuthClientTokenSource(config oauth2.Config, ctx context.Context, authHandler AuthorizationHandler, state string) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, oauthClientSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) +} + +type oauthClientSource struct { + config oauth2.Config + ctx context.Context + authHandler AuthorizationHandler + state string +} + +func (ocs oauthClientSource) Token() (*oauth2.Token, error) { + url := ocs.config.AuthCodeURL(ocs.state) + code, state, err := ocs.authHandler(url) + if err != nil { + return nil, err + } + if state == ocs.state { + return ocs.config.Exchange(ocs.ctx, code) + } + return nil, errors.New("State mismatch in OAuth workflow.") +} From 04f020b1f245738858a4de774b36a64c2cf7cabd Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Thu, 4 Jun 2020 22:27:52 -0700 Subject: [PATCH 02/15] google: Make state configurable in DefaultAuthorizationHandler --- google/authhandler.go | 29 ++++++++++++++++++++++------- google/google.go | 4 ++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/google/authhandler.go b/google/authhandler.go index c92bac83c..5f78a3e49 100644 --- a/google/authhandler.go +++ b/google/authhandler.go @@ -6,18 +6,33 @@ package google import ( "fmt" + + "github.com/google/uuid" ) -const DefaultState = "state" +// RandomAuthorizationState generates a state via UUID generator. +func RandomAuthorizationState() string { + return uuid.New().String() +} -// DefaultAuthorizationHandler is a commandline-based auth handler +// DefaultAuthorizationHandler returns a command line auth handler // that prints the auth URL on the console and prompts the user to // authorize in the browser and paste the auth code back via stdin. -// When using this auth handler, DefaultState must be used. -func DefaultAuthorizationHandler(authCodeUrl string) (string, string, error) { - fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeUrl) - fmt.Println("Enter verification code: ") +// +// For convenience, this handler returns a pre-configured state +// instead of asking the user to additionally paste the state from +// the auth response. In order for this to work, the state +// configured here should match the one in the oauth2 AuthTokenURL. +func DefaultAuthorizationHandler(state string) AuthorizationHandler { + return func(authCodeURL string) (string, string, error) { + return defaultAuthorizationHandlerHelper(state, authCodeURL) + } +} + +func defaultAuthorizationHandlerHelper(state string, authCodeURL string) (string, string, error) { + fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) + fmt.Println("Enter authorization code: ") var code string fmt.Scanln(&code) - return code, DefaultState, nil + return code, state, nil } diff --git a/google/google.go b/google/google.go index ebcabe5bf..ec65bd505 100644 --- a/google/google.go +++ b/google/google.go @@ -220,13 +220,13 @@ type AuthorizationHandler func(string) (string, string, error) // An environment-specific AuthorizationHandler is used to obtain user consent. // Per OAuth protocol, a unique "state" string should be sent and verified // before token exchange to prevent CSRF attacks. -func OAuthClientTokenSource(config oauth2.Config, ctx context.Context, authHandler AuthorizationHandler, state string) oauth2.TokenSource { +func OAuthClientTokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { return oauth2.ReuseTokenSource(nil, oauthClientSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) } type oauthClientSource struct { - config oauth2.Config ctx context.Context + config *oauth2.Config authHandler AuthorizationHandler state string } From 11059998b34553d3f105387e9fb23fceaccb3447 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Tue, 9 Mar 2021 14:05:58 -0800 Subject: [PATCH 03/15] authhandler: Add support for 3-legged-OAuth Added authhandler.go, which implements a TokenSource to support "three-legged OAuth 2.0" via a custom AuthorizationHandler. Added default_authhandler.go to provide a command line implementation for AuthorizationHandler. --- authhandler/authhandler.go | 51 +++++++++++++++++++ .../default_authhandler.go | 24 +++++---- google/google.go | 35 ------------- 3 files changed, 65 insertions(+), 45 deletions(-) create mode 100644 authhandler/authhandler.go rename google/authhandler.go => authhandler/default_authhandler.go (64%) diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go new file mode 100644 index 000000000..da5997579 --- /dev/null +++ b/authhandler/authhandler.go @@ -0,0 +1,51 @@ +// Copyright 2021 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 authhandler implements a TokenSource to support +// "three-legged OAuth 2.0" via a custom AuthorizationHandler. +package authhandler + +import ( + "context" + "errors" + + "golang.org/x/oauth2" +) + +// AuthorizationHandler is a 3-legged-OAuth helper that +// prompts the user for OAuth consent at the specified Auth URL +// and returns an auth code and state upon approval. +type AuthorizationHandler func(string) (string, string, error) + +// TokenSource returns an oauth2.TokenSource that fetches access tokens +// using 3-legged-OAuth flow. +// +// The provided oauth2.Config should be a full configuration containing AuthURL, +// TokenURL, and scope. An environment-specific AuthorizationHandler is used to +// obtain user consent. +// +// Per OAuth protocol, a unique "state" string should be sent and verified +// before exchanging auth code for OAuth token to prevent CSRF attacks. +func TokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { + return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) +} + +type authHandlerSource struct { + ctx context.Context + config *oauth2.Config + authHandler AuthorizationHandler + state string +} + +func (source authHandlerSource) Token() (*oauth2.Token, error) { + url := source.config.AuthCodeURL(source.state) + code, state, err := source.authHandler(url) + if err != nil { + return nil, err + } + if state == source.state { + return source.config.Exchange(source.ctx, code) + } + return nil, errors.New("State mismatch in 3-legged-OAuth flow.") +} diff --git a/google/authhandler.go b/authhandler/default_authhandler.go similarity index 64% rename from google/authhandler.go rename to authhandler/default_authhandler.go index 5f78a3e49..22cb7f8b0 100644 --- a/google/authhandler.go +++ b/authhandler/default_authhandler.go @@ -1,28 +1,32 @@ -// Copyright 2020 The Go Authors. All rights reserved. +// Copyright 2021 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 google +package authhandler import ( "fmt" - - "github.com/google/uuid" ) -// RandomAuthorizationState generates a state via UUID generator. -func RandomAuthorizationState() string { - return uuid.New().String() -} - // DefaultAuthorizationHandler returns a command line auth handler // that prints the auth URL on the console and prompts the user to // authorize in the browser and paste the auth code back via stdin. // +// Per OAuth protocol, a unique "state" string should be sent and verified +// before exchanging auth code for OAuth token to prevent CSRF attacks. +// // For convenience, this handler returns a pre-configured state // instead of asking the user to additionally paste the state from // the auth response. In order for this to work, the state -// configured here should match the one in the oauth2 AuthTokenURL. +// configured here must match the state used in authCodeURL. +// +// Usage example: +// +// state := uuid.New().String() +// tokenSource:= authhandler.TokenSource(ctx, conf +// authhandler.DefaultAuthorizationHandler(state), state) +// pubsubService, err := pubsub.NewService(ctx, +// option.WithTokenSource(tokenSource)) func DefaultAuthorizationHandler(state string) AuthorizationHandler { return func(authCodeURL string) (string, string, error) { return defaultAuthorizationHandlerHelper(state, authCodeURL) diff --git a/google/google.go b/google/google.go index ec65bd505..81de32b36 100644 --- a/google/google.go +++ b/google/google.go @@ -207,38 +207,3 @@ func (cs computeSource) Token() (*oauth2.Token, error) { "oauth2.google.serviceAccount": acct, }), nil } - -// AuthorizationHandler is a 3-legged-OAuth helper that -// prompts the user for OAuth consent at the specified Auth URL -// and returns an auth code and state upon approval. -type AuthorizationHandler func(string) (string, string, error) - -// OAuthClientTokenSource returns an oauth2.TokenSource that fetches access tokens -// using 3-legged-OAuth workflow. -// The provided oauth2.Config should be a full configuration containing AuthURL, -// TokenURL, and scope. -// An environment-specific AuthorizationHandler is used to obtain user consent. -// Per OAuth protocol, a unique "state" string should be sent and verified -// before token exchange to prevent CSRF attacks. -func OAuthClientTokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { - return oauth2.ReuseTokenSource(nil, oauthClientSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) -} - -type oauthClientSource struct { - ctx context.Context - config *oauth2.Config - authHandler AuthorizationHandler - state string -} - -func (ocs oauthClientSource) Token() (*oauth2.Token, error) { - url := ocs.config.AuthCodeURL(ocs.state) - code, state, err := ocs.authHandler(url) - if err != nil { - return nil, err - } - if state == ocs.state { - return ocs.config.Exchange(ocs.ctx, code) - } - return nil, errors.New("State mismatch in OAuth workflow.") -} From cde11fb8409d2ce2a09ed36751269bc37d9e7211 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Thu, 11 Mar 2021 21:22:41 -0800 Subject: [PATCH 04/15] authhandler: Add authhandler_test.go --- authhandler/authhandler.go | 19 ++++--- authhandler/authhandler_test.go | 99 +++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 authhandler/authhandler_test.go diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go index da5997579..0b53f15d5 100644 --- a/authhandler/authhandler.go +++ b/authhandler/authhandler.go @@ -13,20 +13,23 @@ import ( "golang.org/x/oauth2" ) -// AuthorizationHandler is a 3-legged-OAuth helper that -// prompts the user for OAuth consent at the specified Auth URL +// AuthorizationHandler is a 3-legged-OAuth helper that prompts +// the user for OAuth consent at the specified auth code URL // and returns an auth code and state upon approval. -type AuthorizationHandler func(string) (string, string, error) +type AuthorizationHandler func(authCodeURL string) (code string, state string, err error) // TokenSource returns an oauth2.TokenSource that fetches access tokens // using 3-legged-OAuth flow. // +// The provided context.Context is used for oauth2 Exchange operation. +// // The provided oauth2.Config should be a full configuration containing AuthURL, -// TokenURL, and scope. An environment-specific AuthorizationHandler is used to -// obtain user consent. +// TokenURL, and Scope. +// +// An environment-specific AuthorizationHandler is used to obtain user consent. // -// Per OAuth protocol, a unique "state" string should be sent and verified -// before exchanging auth code for OAuth token to prevent CSRF attacks. +// Per the OAuth protocol, a unique "state" string should be sent and verified +// before exchanging the auth code for OAuth token to prevent CSRF attacks. func TokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) } @@ -47,5 +50,5 @@ func (source authHandlerSource) Token() (*oauth2.Token, error) { if state == source.state { return source.config.Exchange(source.ctx, code) } - return nil, errors.New("State mismatch in 3-legged-OAuth flow.") + return nil, errors.New("state mismatch in 3-legged-OAuth flow.") } diff --git a/authhandler/authhandler_test.go b/authhandler/authhandler_test.go new file mode 100644 index 000000000..8ba6ca624 --- /dev/null +++ b/authhandler/authhandler_test.go @@ -0,0 +1,99 @@ +// Copyright 2021 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 authhandler + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "golang.org/x/oauth2" +) + +func TestTokenExchange_Success(t *testing.T) { + authhandler := func(authCodeURL string) (string, string, error) { + if authCodeURL == "testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=testState" { + return "testCode", "testState", nil + } + return "", "", fmt.Errorf("invalid authCodeURL: %q", authCodeURL) + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + if r.Form.Get("code") == "testCode" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + } + })) + defer ts.Close() + + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + + tok, err := TokenSource(context.Background(), conf, authhandler, "testState").Token() + if err != nil { + t.Fatal(err) + } + if !tok.Valid() { + t.Errorf("got invalid token: %v", tok) + } + if got, want := tok.AccessToken, "90d64460d14870c08c81352a05dedd3465940a7c"; got != want { + t.Errorf("access token = %q; want %q", got, want) + } + if got, want := tok.TokenType, "bearer"; got != want { + t.Errorf("token type = %q; want %q", got, want) + } + if got := tok.Expiry.IsZero(); got { + t.Errorf("token expiry is zero = %v, want false", got) + } + scope := tok.Extra("scope") + if got, want := scope, "pubsub"; got != want { + t.Errorf("scope = %q; want %q", got, want) + } +} + +func TestTokenExchange_StateMismatch(t *testing.T) { + authhandler := func(authCodeURL string) (string, string, error) { + return "testCode", "testStateMismatch", nil + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + })) + defer ts.Close() + + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + + _, err := TokenSource(context.Background(), conf, authhandler, "testState").Token() + if want_err := "state mismatch in 3-legged-OAuth flow."; err == nil || err.Error() != want_err { + t.Errorf("err = %q; want %q", err, want_err) + } +} From 7c289229aeb96c036d0bda0f459b78630c8c7aeb Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Thu, 11 Mar 2021 21:36:49 -0800 Subject: [PATCH 05/15] authhandler: Update default_authhandler.go to inline handler logic --- authhandler/default_authhandler.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/authhandler/default_authhandler.go b/authhandler/default_authhandler.go index 22cb7f8b0..1989f31c1 100644 --- a/authhandler/default_authhandler.go +++ b/authhandler/default_authhandler.go @@ -29,14 +29,10 @@ import ( // option.WithTokenSource(tokenSource)) func DefaultAuthorizationHandler(state string) AuthorizationHandler { return func(authCodeURL string) (string, string, error) { - return defaultAuthorizationHandlerHelper(state, authCodeURL) + fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) + fmt.Println("Enter authorization code: ") + var code string + fmt.Scanln(&code) + return code, state, nil } } - -func defaultAuthorizationHandlerHelper(state string, authCodeURL string) (string, string, error) { - fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) - fmt.Println("Enter authorization code: ") - var code string - fmt.Scanln(&code) - return code, state, nil -} From 8a926e1234bba3432adfb2b46a14ff40d0d0f96a Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Mon, 15 Mar 2021 13:45:25 -0700 Subject: [PATCH 06/15] authhandler: Rename to CmdAuthorizationHandler and add example_test.go --- ...ault_authhandler.go => cmd_authhandler.go} | 14 +---- authhandler/example_test.go | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 11 deletions(-) rename authhandler/{default_authhandler.go => cmd_authhandler.go} (68%) create mode 100644 authhandler/example_test.go diff --git a/authhandler/default_authhandler.go b/authhandler/cmd_authhandler.go similarity index 68% rename from authhandler/default_authhandler.go rename to authhandler/cmd_authhandler.go index 1989f31c1..450040be5 100644 --- a/authhandler/default_authhandler.go +++ b/authhandler/cmd_authhandler.go @@ -8,7 +8,7 @@ import ( "fmt" ) -// DefaultAuthorizationHandler returns a command line auth handler +// CmdAuthorizationHandler returns a command line auth handler // that prints the auth URL on the console and prompts the user to // authorize in the browser and paste the auth code back via stdin. // @@ -19,18 +19,10 @@ import ( // instead of asking the user to additionally paste the state from // the auth response. In order for this to work, the state // configured here must match the state used in authCodeURL. -// -// Usage example: -// -// state := uuid.New().String() -// tokenSource:= authhandler.TokenSource(ctx, conf -// authhandler.DefaultAuthorizationHandler(state), state) -// pubsubService, err := pubsub.NewService(ctx, -// option.WithTokenSource(tokenSource)) -func DefaultAuthorizationHandler(state string) AuthorizationHandler { +func CmdAuthorizationHandler(state string) AuthorizationHandler { return func(authCodeURL string) (string, string, error) { fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) - fmt.Println("Enter authorization code: ") + fmt.Println("Enter authorization code:") var code string fmt.Scanln(&code) return code, state, nil diff --git a/authhandler/example_test.go b/authhandler/example_test.go new file mode 100644 index 000000000..99456efd7 --- /dev/null +++ b/authhandler/example_test.go @@ -0,0 +1,56 @@ +// Copyright 2021 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 authhandler_test + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/authhandler" +) + +func ExampleCmdAuthorizationHandler() { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", + "scope": "pubsub", + "token_type": "bearer", + "expires_in": 3600 + }`)) + })) + defer ts.Close() + + ctx := context.Background() + conf := &oauth2.Config{ + ClientID: "testClientID", + Scopes: []string{"pubsub"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "testAuthCodeURL", + TokenURL: ts.URL, + }, + } + state := "unique_state" + + token, err := authhandler.TokenSource(ctx, conf, authhandler.CmdAuthorizationHandler(state), state).Token() + + if err != nil { + fmt.Println(err) + } + + fmt.Printf("AccessToken: %s", token.AccessToken) + + // Output: + // Go to the following link in your browser: + // + // testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=unique_state + // + // Enter authorization code: + // AccessToken: 90d64460d14870c08c81352a05dedd3465940a7c +} From ab12fee4d10d255ed365981b5993cbb97a5e1787 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Wed, 17 Mar 2021 12:29:45 -0700 Subject: [PATCH 07/15] authhandler: Make authHandler the last parameter --- authhandler/authhandler.go | 8 ++++---- authhandler/authhandler_test.go | 6 +++--- authhandler/example_test.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go index 0b53f15d5..e3973461a 100644 --- a/authhandler/authhandler.go +++ b/authhandler/authhandler.go @@ -30,7 +30,7 @@ type AuthorizationHandler func(authCodeURL string) (code string, state string, e // // Per the OAuth protocol, a unique "state" string should be sent and verified // before exchanging the auth code for OAuth token to prevent CSRF attacks. -func TokenSource(ctx context.Context, config *oauth2.Config, authHandler AuthorizationHandler, state string) oauth2.TokenSource { +func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource { return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) } @@ -47,8 +47,8 @@ func (source authHandlerSource) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - if state == source.state { - return source.config.Exchange(source.ctx, code) + if state != source.state { + return nil, errors.New("state mismatch in 3-legged-OAuth flow") } - return nil, errors.New("state mismatch in 3-legged-OAuth flow.") + return source.config.Exchange(source.ctx, code) } diff --git a/authhandler/authhandler_test.go b/authhandler/authhandler_test.go index 8ba6ca624..084198f4c 100644 --- a/authhandler/authhandler_test.go +++ b/authhandler/authhandler_test.go @@ -45,7 +45,7 @@ func TestTokenExchange_Success(t *testing.T) { }, } - tok, err := TokenSource(context.Background(), conf, authhandler, "testState").Token() + tok, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() if err != nil { t.Fatal(err) } @@ -92,8 +92,8 @@ func TestTokenExchange_StateMismatch(t *testing.T) { }, } - _, err := TokenSource(context.Background(), conf, authhandler, "testState").Token() - if want_err := "state mismatch in 3-legged-OAuth flow."; err == nil || err.Error() != want_err { + _, err := TokenSource(context.Background(), conf, "testState", authhandler).Token() + if want_err := "state mismatch in 3-legged-OAuth flow"; err == nil || err.Error() != want_err { t.Errorf("err = %q; want %q", err, want_err) } } diff --git a/authhandler/example_test.go b/authhandler/example_test.go index 99456efd7..ef4cf3183 100644 --- a/authhandler/example_test.go +++ b/authhandler/example_test.go @@ -38,7 +38,7 @@ func ExampleCmdAuthorizationHandler() { } state := "unique_state" - token, err := authhandler.TokenSource(ctx, conf, authhandler.CmdAuthorizationHandler(state), state).Token() + token, err := authhandler.TokenSource(ctx, conf, state, authhandler.CmdAuthorizationHandler(state)).Token() if err != nil { fmt.Println(err) From fcaf0780fa2407dbf4f36e9b9fe090049f56d401 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Thu, 18 Mar 2021 09:14:52 -0700 Subject: [PATCH 08/15] authhandler: Remove CmdAuthorizationHandler and use it as example instead --- authhandler/cmd_authhandler.go | 30 ------------------------------ authhandler/example_test.go | 25 +++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 authhandler/cmd_authhandler.go diff --git a/authhandler/cmd_authhandler.go b/authhandler/cmd_authhandler.go deleted file mode 100644 index 450040be5..000000000 --- a/authhandler/cmd_authhandler.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 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 authhandler - -import ( - "fmt" -) - -// CmdAuthorizationHandler returns a command line auth handler -// that prints the auth URL on the console and prompts the user to -// authorize in the browser and paste the auth code back via stdin. -// -// Per OAuth protocol, a unique "state" string should be sent and verified -// before exchanging auth code for OAuth token to prevent CSRF attacks. -// -// For convenience, this handler returns a pre-configured state -// instead of asking the user to additionally paste the state from -// the auth response. In order for this to work, the state -// configured here must match the state used in authCodeURL. -func CmdAuthorizationHandler(state string) AuthorizationHandler { - return func(authCodeURL string) (string, string, error) { - fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) - fmt.Println("Enter authorization code:") - var code string - fmt.Scanln(&code) - return code, state, nil - } -} diff --git a/authhandler/example_test.go b/authhandler/example_test.go index ef4cf3183..f450590b6 100644 --- a/authhandler/example_test.go +++ b/authhandler/example_test.go @@ -14,7 +14,28 @@ import ( "golang.org/x/oauth2/authhandler" ) -func ExampleCmdAuthorizationHandler() { +// CmdAuthorizationHandler returns a command line auth handler that prints +// the auth URL to the console and prompts the user to authorize in the +// browser and paste the auth code back via stdin. +// +// Per the OAuth protocol, a unique "state" string should be sent and verified +// before exchanging auth code for OAuth token to prevent CSRF attacks. +// +// For convenience, this handler returns a pre-configured state instead of +// asking the user to additionally paste the state from the auth response. +// In order for this to work, the state configured here must match the state +// used in authCodeURL. +func CmdAuthorizationHandler(state string) authhandler.AuthorizationHandler { + return func(authCodeURL string) (string, string, error) { + fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) + fmt.Println("Enter authorization code:") + var code string + fmt.Scanln(&code) + return code, state, nil + } +} + +func Example() { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.ParseForm() w.Header().Set("Content-Type", "application/json") @@ -38,7 +59,7 @@ func ExampleCmdAuthorizationHandler() { } state := "unique_state" - token, err := authhandler.TokenSource(ctx, conf, state, authhandler.CmdAuthorizationHandler(state)).Token() + token, err := authhandler.TokenSource(ctx, conf, state, CmdAuthorizationHandler(state)).Token() if err != nil { fmt.Println(err) From 1ae374609f2f078c4f0988aedeb9833ff27aaca4 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Fri, 19 Mar 2021 11:49:16 -0700 Subject: [PATCH 09/15] authhandler: Reword comment regarding state --- authhandler/authhandler.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/authhandler/authhandler.go b/authhandler/authhandler.go index e3973461a..69967cf87 100644 --- a/authhandler/authhandler.go +++ b/authhandler/authhandler.go @@ -28,8 +28,10 @@ type AuthorizationHandler func(authCodeURL string) (code string, state string, e // // An environment-specific AuthorizationHandler is used to obtain user consent. // -// Per the OAuth protocol, a unique "state" string should be sent and verified -// before exchanging the auth code for OAuth token to prevent CSRF attacks. +// Per the OAuth protocol, a unique "state" string should be specified here. +// This token source will verify that the "state" is identical in the request +// and response before exchanging the auth code for OAuth token to prevent CSRF +// attacks. func TokenSource(ctx context.Context, config *oauth2.Config, state string, authHandler AuthorizationHandler) oauth2.TokenSource { return oauth2.ReuseTokenSource(nil, authHandlerSource{config: config, ctx: ctx, authHandler: authHandler, state: state}) } From 48fc0367c2092baf97b8e09f03a94e7fe1ecd890 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Fri, 19 Mar 2021 11:53:37 -0700 Subject: [PATCH 10/15] authhandler: Reword comment regarding state in example --- authhandler/example_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/authhandler/example_test.go b/authhandler/example_test.go index f450590b6..a62b4e133 100644 --- a/authhandler/example_test.go +++ b/authhandler/example_test.go @@ -18,8 +18,10 @@ import ( // the auth URL to the console and prompts the user to authorize in the // browser and paste the auth code back via stdin. // -// Per the OAuth protocol, a unique "state" string should be sent and verified -// before exchanging auth code for OAuth token to prevent CSRF attacks. +// Per the OAuth protocol, a unique "state" string should be specified here. +// The authhandler token source will verify that the "state" is identical in +// the request and response before exchanging the auth code for OAuth token to +// prevent CSRF attacks. // // For convenience, this handler returns a pre-configured state instead of // asking the user to additionally paste the state from the auth response. From ca291c568f483957ce5d59a4848da207ed68b049 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Mon, 12 Apr 2021 10:11:51 -0700 Subject: [PATCH 11/15] authhandler: Remove example_test.go --- authhandler/example_test.go | 79 ------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 authhandler/example_test.go diff --git a/authhandler/example_test.go b/authhandler/example_test.go deleted file mode 100644 index a62b4e133..000000000 --- a/authhandler/example_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2021 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 authhandler_test - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/authhandler" -) - -// CmdAuthorizationHandler returns a command line auth handler that prints -// the auth URL to the console and prompts the user to authorize in the -// browser and paste the auth code back via stdin. -// -// Per the OAuth protocol, a unique "state" string should be specified here. -// The authhandler token source will verify that the "state" is identical in -// the request and response before exchanging the auth code for OAuth token to -// prevent CSRF attacks. -// -// For convenience, this handler returns a pre-configured state instead of -// asking the user to additionally paste the state from the auth response. -// In order for this to work, the state configured here must match the state -// used in authCodeURL. -func CmdAuthorizationHandler(state string) authhandler.AuthorizationHandler { - return func(authCodeURL string) (string, string, error) { - fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) - fmt.Println("Enter authorization code:") - var code string - fmt.Scanln(&code) - return code, state, nil - } -} - -func Example() { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{ - "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", - "scope": "pubsub", - "token_type": "bearer", - "expires_in": 3600 - }`)) - })) - defer ts.Close() - - ctx := context.Background() - conf := &oauth2.Config{ - ClientID: "testClientID", - Scopes: []string{"pubsub"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "testAuthCodeURL", - TokenURL: ts.URL, - }, - } - state := "unique_state" - - token, err := authhandler.TokenSource(ctx, conf, state, CmdAuthorizationHandler(state)).Token() - - if err != nil { - fmt.Println(err) - } - - fmt.Printf("AccessToken: %s", token.AccessToken) - - // Output: - // Go to the following link in your browser: - // - // testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=unique_state - // - // Enter authorization code: - // AccessToken: 90d64460d14870c08c81352a05dedd3465940a7c -} From baf46329c243ccbe87cc822baaac86f71f45682c Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Mon, 12 Apr 2021 10:21:24 -0700 Subject: [PATCH 12/15] authhandler: Remove example_test.go --- authhandler/example_test.go | 79 ------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 authhandler/example_test.go diff --git a/authhandler/example_test.go b/authhandler/example_test.go deleted file mode 100644 index a62b4e133..000000000 --- a/authhandler/example_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2021 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 authhandler_test - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/authhandler" -) - -// CmdAuthorizationHandler returns a command line auth handler that prints -// the auth URL to the console and prompts the user to authorize in the -// browser and paste the auth code back via stdin. -// -// Per the OAuth protocol, a unique "state" string should be specified here. -// The authhandler token source will verify that the "state" is identical in -// the request and response before exchanging the auth code for OAuth token to -// prevent CSRF attacks. -// -// For convenience, this handler returns a pre-configured state instead of -// asking the user to additionally paste the state from the auth response. -// In order for this to work, the state configured here must match the state -// used in authCodeURL. -func CmdAuthorizationHandler(state string) authhandler.AuthorizationHandler { - return func(authCodeURL string) (string, string, error) { - fmt.Printf("Go to the following link in your browser:\n\n %s\n\n", authCodeURL) - fmt.Println("Enter authorization code:") - var code string - fmt.Scanln(&code) - return code, state, nil - } -} - -func Example() { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.ParseForm() - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{ - "access_token": "90d64460d14870c08c81352a05dedd3465940a7c", - "scope": "pubsub", - "token_type": "bearer", - "expires_in": 3600 - }`)) - })) - defer ts.Close() - - ctx := context.Background() - conf := &oauth2.Config{ - ClientID: "testClientID", - Scopes: []string{"pubsub"}, - Endpoint: oauth2.Endpoint{ - AuthURL: "testAuthCodeURL", - TokenURL: ts.URL, - }, - } - state := "unique_state" - - token, err := authhandler.TokenSource(ctx, conf, state, CmdAuthorizationHandler(state)).Token() - - if err != nil { - fmt.Println(err) - } - - fmt.Printf("AccessToken: %s", token.AccessToken) - - // Output: - // Go to the following link in your browser: - // - // testAuthCodeURL?client_id=testClientID&response_type=code&scope=pubsub&state=unique_state - // - // Enter authorization code: - // AccessToken: 90d64460d14870c08c81352a05dedd3465940a7c -} From 9b7db16e5ee9712c19e5539cb92957cfc38290c0 Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Mon, 19 Apr 2021 12:13:37 -0700 Subject: [PATCH 13/15] google: Add support for TokenSourceParams, Client ID json, and JWT Subject --- google/default.go | 68 ++++++++++++++++++++++++++++++++++++++--------- google/google.go | 30 ++++++++++++++------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/google/default.go b/google/default.go index ae391313d..1074a1c1e 100644 --- a/google/default.go +++ b/google/default.go @@ -16,6 +16,7 @@ import ( "cloud.google.com/go/compute/metadata" "golang.org/x/oauth2" + "golang.org/x/oauth2/authhandler" ) // Credentials holds Google credentials, including "Application Default Credentials". @@ -41,6 +42,22 @@ type Credentials struct { // Deprecated: use Credentials instead. type DefaultCredentials = Credentials +// TokenSourceParams holds user supplied parameters that are +// used for building a TokenSource. +type TokenSourceParams struct { + // List of OAuth scopes. + Scopes []string + + // User email used for domain wide delegation. + Subject string + + // AuthorizationHandler used for 3-legged OAuth flow. + AuthHandler authhandler.AuthorizationHandler + + // State used for AuthorizationHandler. + State string +} + // DefaultClient returns an HTTP Client that uses the // DefaultTokenSource to obtain authentication credentials. func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { @@ -81,11 +98,11 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc // 4. On Google Compute Engine, Google App Engine standard second generation runtimes // (>= Go 1.11), and Google App Engine flexible environment, it fetches // credentials from the metadata server. -func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { +func FindDefaultCredentialsWithParams(ctx context.Context, params TokenSourceParams) (*Credentials, error) { // First, try the environment variable. const envVar = "GOOGLE_APPLICATION_CREDENTIALS" if filename := os.Getenv(envVar); filename != "" { - creds, err := readCredentialsFile(ctx, filename, scopes) + creds, err := readCredentialsFile(ctx, filename, params) if err != nil { return nil, fmt.Errorf("google: error getting credentials using %v environment variable: %v", envVar, err) } @@ -94,7 +111,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials // Second, try a well-known file. filename := wellKnownFile() - if creds, err := readCredentialsFile(ctx, filename, scopes); err == nil { + if creds, err := readCredentialsFile(ctx, filename, params); err == nil { return creds, nil } else if !os.IsNotExist(err) { return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err) @@ -106,7 +123,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials if appengineTokenFunc != nil { return &DefaultCredentials{ ProjectID: appengineAppIDFunc(ctx), - TokenSource: AppEngineTokenSource(ctx, scopes...), + TokenSource: AppEngineTokenSource(ctx, params.Scopes...), }, nil } @@ -116,7 +133,7 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials id, _ := metadata.ProjectID() return &DefaultCredentials{ ProjectID: id, - TokenSource: ComputeTokenSource("", scopes...), + TokenSource: ComputeTokenSource("", params.Scopes...), }, nil } @@ -125,18 +142,36 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) } +// Deprecated: use FindDefaultCredentialsWithParams instead. +func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { + var params TokenSourceParams + params.Scopes = scopes + return FindDefaultCredentialsWithParams(ctx, params) +} + // CredentialsFromJSON 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 (as in -// JWTConfigFromJSON) or the JSON configuration file for workload identity federation -// in non-Google cloud platforms (see +// ConfigFromJSON), a Google Developers service account key file, a gCloud-style +// 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). -func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { +func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params TokenSourceParams) (*Credentials, error) { + // First, attempt to parse jsonData as a Google Developers Console client_credentials.json. + config, err := ConfigFromJSON(jsonData, params.Scopes...) + if err == nil { + return &Credentials{ + ProjectID: "", + TokenSource: authhandler.TokenSource(ctx, config, params.State, params.AuthHandler), + JSON: jsonData, + }, nil + } + + // Otherwise, parse jsonData as one of the other supported credential files. var f credentialsFile - if err := json.Unmarshal(jsonData, &f); err != nil { + if err = json.Unmarshal(jsonData, &f); err != nil { return nil, err } - ts, err := f.tokenSource(ctx, append([]string(nil), scopes...)) + ts, err := f.tokenSource(ctx, params) if err != nil { return nil, err } @@ -147,6 +182,13 @@ func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) }, nil } +// Deprecated: use CredentialsFromJSONWithParams instead. +func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { + var params TokenSourceParams + params.Scopes = scopes + return CredentialsFromJSONWithParams(ctx, jsonData, params) +} + func wellKnownFile() string { const f = "application_default_credentials.json" if runtime.GOOS == "windows" { @@ -155,10 +197,10 @@ func wellKnownFile() string { return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) } -func readCredentialsFile(ctx context.Context, filename string, scopes []string) (*DefaultCredentials, error) { +func readCredentialsFile(ctx context.Context, filename string, params TokenSourceParams) (*DefaultCredentials, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err } - return CredentialsFromJSON(ctx, b, scopes...) + return CredentialsFromJSONWithParams(ctx, b, params) } diff --git a/google/google.go b/google/google.go index 2c8f1bd5a..180260682 100644 --- a/google/google.go +++ b/google/google.go @@ -19,7 +19,7 @@ import ( "golang.org/x/oauth2/jwt" ) -// Endpoint is Google's OAuth 2.0 endpoint. +// Endpoint is Google's OAuth 2.0 default endpoint. var Endpoint = oauth2.Endpoint{ AuthURL: "https://accounts.google.com/o/oauth2/auth", TokenURL: "https://oauth2.googleapis.com/token", @@ -87,7 +87,7 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) { return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey) } scope = append([]string(nil), scope...) // copy - return f.jwtConfig(scope), nil + return f.jwtConfig(scope, ""), nil } // JSON key file types. @@ -99,12 +99,13 @@ const ( // credentialsFile is the unmarshalled representation of a credentials file. type credentialsFile struct { - Type string `json:"type"` // serviceAccountKey or userCredentialsKey + Type string `json:"type"` // Service Account fields ClientEmail string `json:"client_email"` PrivateKeyID string `json:"private_key_id"` PrivateKey string `json:"private_key"` + AuthURL string `json:"auth_uri"` TokenURL string `json:"token_uri"` ProjectID string `json:"project_id"` @@ -124,13 +125,14 @@ type credentialsFile struct { QuotaProjectID string `json:"quota_project_id"` } -func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { +func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config { cfg := &jwt.Config{ Email: f.ClientEmail, PrivateKey: []byte(f.PrivateKey), PrivateKeyID: f.PrivateKeyID, Scopes: scopes, TokenURL: f.TokenURL, + Subject: subject, // This is the user email to impersonate } if cfg.TokenURL == "" { cfg.TokenURL = JWTTokenURL @@ -138,17 +140,27 @@ func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config { return cfg } -func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) { +func (f *credentialsFile) tokenSource(ctx context.Context, params TokenSourceParams) (oauth2.TokenSource, error) { switch f.Type { case serviceAccountKey: - cfg := f.jwtConfig(scopes) + cfg := f.jwtConfig(params.Scopes, params.Subject) return cfg.TokenSource(ctx), nil case userCredentialsKey: cfg := &oauth2.Config{ ClientID: f.ClientID, ClientSecret: f.ClientSecret, - Scopes: scopes, - Endpoint: Endpoint, + Scopes: params.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: f.AuthURL, + TokenURL: f.TokenURL, + AuthStyle: oauth2.AuthStyleInParams, + }, + } + if cfg.Endpoint.AuthURL == "" { + cfg.Endpoint.AuthURL = Endpoint.AuthURL + } + if cfg.Endpoint.TokenURL == "" { + cfg.Endpoint.TokenURL = Endpoint.TokenURL } tok := &oauth2.Token{RefreshToken: f.RefreshToken} return cfg.TokenSource(ctx, tok), nil @@ -163,7 +175,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oau ClientID: f.ClientID, CredentialSource: f.CredentialSource, QuotaProjectID: f.QuotaProjectID, - Scopes: scopes, + Scopes: params.Scopes, } return cfg.TokenSource(ctx), nil case "": From 3a734b4293d521c4e596ade9bdec8f905b16623a Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Wed, 21 Apr 2021 09:20:55 -0700 Subject: [PATCH 14/15] google: Address comments for CredentialsParams support --- google/default.go | 59 +++++++++++++++++++++++++++++------------------ google/google.go | 2 +- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/google/default.go b/google/default.go index 1074a1c1e..9fd692b8a 100644 --- a/google/default.go +++ b/google/default.go @@ -42,22 +42,32 @@ type Credentials struct { // Deprecated: use Credentials instead. type DefaultCredentials = Credentials -// TokenSourceParams holds user supplied parameters that are -// used for building a TokenSource. -type TokenSourceParams struct { - // List of OAuth scopes. +// CredentialsParams holds user supplied parameters that are used together +// with a credentials file for building a Credentials object. +type CredentialsParams struct { + // Scopes is the list OAuth scopes. Required. + // Example: cloud-platform Scopes []string - // User email used for domain wide delegation. + // Subject is the user email used for domain wide delegation (see + // https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority). + // Optional. Subject string - // AuthorizationHandler used for 3-legged OAuth flow. + // AuthHandler is the AuthorizationHandler used for 3-legged OAuth flow. Optional. AuthHandler authhandler.AuthorizationHandler - // State used for AuthorizationHandler. + // State is a unique string used with AuthHandler. Optional. State string } +func (params CredentialsParams) deepCopy() CredentialsParams { + paramsCopy := params + paramsCopy.Scopes = make([]string, len(params.Scopes)) + copy(paramsCopy.Scopes, params.Scopes) + return paramsCopy +} + // DefaultClient returns an HTTP Client that uses the // DefaultTokenSource to obtain authentication credentials. func DefaultClient(ctx context.Context, scope ...string) (*http.Client, error) { @@ -79,7 +89,7 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc return creds.TokenSource, nil } -// FindDefaultCredentials searches for "Application Default Credentials". +// FindDefaultCredentialsWithParams searches for "Application Default Credentials". // // It looks for credentials in the following places, // preferring the first location found: @@ -98,7 +108,10 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc // 4. On Google Compute Engine, Google App Engine standard second generation runtimes // (>= Go 1.11), and Google App Engine flexible environment, it fetches // credentials from the metadata server. -func FindDefaultCredentialsWithParams(ctx context.Context, params TokenSourceParams) (*Credentials, error) { +func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) { + // Make defensive copy of the slices in params. + params = params.deepCopy() + // First, try the environment variable. const envVar = "GOOGLE_APPLICATION_CREDENTIALS" if filename := os.Getenv(envVar); filename != "" { @@ -142,20 +155,22 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params TokenSourcePar return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url) } -// Deprecated: use FindDefaultCredentialsWithParams instead. +// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes. func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials, error) { - var params TokenSourceParams + var params CredentialsParams params.Scopes = scopes return FindDefaultCredentialsWithParams(ctx, params) } -// CredentialsFromJSON 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-style -// 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). -func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params TokenSourceParams) (*Credentials, error) { +// 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-style 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). +func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) { + // Make defensive copy of the slices in params. + params = params.deepCopy() + // First, attempt to parse jsonData as a Google Developers Console client_credentials.json. config, err := ConfigFromJSON(jsonData, params.Scopes...) if err == nil { @@ -166,7 +181,7 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params }, nil } - // Otherwise, parse jsonData as one of the other supported credential files. + // Otherwise, parse jsonData as one of the other supported credentials files. var f credentialsFile if err = json.Unmarshal(jsonData, &f); err != nil { return nil, err @@ -182,9 +197,9 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params }, nil } -// Deprecated: use CredentialsFromJSONWithParams instead. +// CredentialsFromJSON invokes CredentialsFromJSONWithParams with the specified scopes. func CredentialsFromJSON(ctx context.Context, jsonData []byte, scopes ...string) (*Credentials, error) { - var params TokenSourceParams + var params CredentialsParams params.Scopes = scopes return CredentialsFromJSONWithParams(ctx, jsonData, params) } @@ -197,7 +212,7 @@ func wellKnownFile() string { return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f) } -func readCredentialsFile(ctx context.Context, filename string, params TokenSourceParams) (*DefaultCredentials, error) { +func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*DefaultCredentials, error) { b, err := ioutil.ReadFile(filename) if err != nil { return nil, err diff --git a/google/google.go b/google/google.go index 180260682..2b631f522 100644 --- a/google/google.go +++ b/google/google.go @@ -140,7 +140,7 @@ func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config return cfg } -func (f *credentialsFile) tokenSource(ctx context.Context, params TokenSourceParams) (oauth2.TokenSource, error) { +func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsParams) (oauth2.TokenSource, error) { switch f.Type { case serviceAccountKey: cfg := f.jwtConfig(params.Scopes, params.Subject) From eb92ab4a212ad9ca72837d6e89e3c5f7e106a51c Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Fri, 23 Apr 2021 10:25:37 -0700 Subject: [PATCH 15/15] google: Address additional comments for CredentialsParams support. --- google/default.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/google/default.go b/google/default.go index 9fd692b8a..880dd7b59 100644 --- a/google/default.go +++ b/google/default.go @@ -46,7 +46,7 @@ type DefaultCredentials = Credentials // with a credentials file for building a Credentials object. type CredentialsParams struct { // Scopes is the list OAuth scopes. Required. - // Example: cloud-platform + // Example: https://www.googleapis.com/auth/cloud-platform Scopes []string // Subject is the user email used for domain wide delegation (see @@ -164,16 +164,16 @@ func FindDefaultCredentials(ctx context.Context, scopes ...string) (*Credentials // 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-style 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). +// 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). func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params CredentialsParams) (*Credentials, error) { // Make defensive copy of the slices in params. params = params.deepCopy() // First, attempt to parse jsonData as a Google Developers Console client_credentials.json. - config, err := ConfigFromJSON(jsonData, params.Scopes...) - if err == nil { + config, _ := ConfigFromJSON(jsonData, params.Scopes...) + if config != nil { return &Credentials{ ProjectID: "", TokenSource: authhandler.TokenSource(ctx, config, params.State, params.AuthHandler), @@ -183,7 +183,7 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params // Otherwise, parse jsonData as one of the other supported credentials files. var f credentialsFile - if err = json.Unmarshal(jsonData, &f); err != nil { + if err := json.Unmarshal(jsonData, &f); err != nil { return nil, err } ts, err := f.tokenSource(ctx, params)