From ceaa866219bd09735d89c4e66e48b90534c2ef8f Mon Sep 17 00:00:00 2001 From: Andy Zhao Date: Tue, 5 May 2020 16:55:31 -0700 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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.