From 027a6963cdd15a98e04fe45b622f6041ae07f382 Mon Sep 17 00:00:00 2001 From: Robin Trietsch Date: Fri, 9 Dec 2022 10:31:32 +0100 Subject: [PATCH 1/5] feat: add support for non-interactive logins --- non_interactive.go | 32 ++++++++++++++++++++++++++++++++ oauth2cli.go | 36 ++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 non_interactive.go diff --git a/non_interactive.go b/non_interactive.go new file mode 100644 index 0000000..ed99915 --- /dev/null +++ b/non_interactive.go @@ -0,0 +1,32 @@ +package oauth2cli + +import ( + "bufio" + "fmt" + "golang.org/x/sync/errgroup" + "os" +) + +func receiveCodeViaUserInput(c *Config) (string, error) { + var userInput string + + var eg errgroup.Group + + eg.Go(func() error { + buf := bufio.NewReader(os.Stdin) + fmt.Print(c.NonInteractivePromptText) + input, err := buf.ReadBytes('\n') + if err != nil { + return err + } else { + userInput = string(input) + return nil + } + }) + + if err := eg.Wait(); err != nil { + return "", fmt.Errorf("non-interactive authorization error: %w", err) + } + + return userInput, nil +} diff --git a/oauth2cli.go b/oauth2cli.go index 578ecd3..c013b39 100644 --- a/oauth2cli.go +++ b/oauth2cli.go @@ -94,6 +94,11 @@ type Config struct { // Redirect URL upon failed login FailureRedirectURL string + // Allow non-interactive login flows (headless machines without a browser available) + NonInteractive bool + // Instructions to print to stdout for non-interactive flows + NonInteractivePromptText string + // Logger function for debug. Logf func(format string, args ...interface{}) } @@ -127,6 +132,9 @@ func (c *Config) validateAndSetDefaults() error { (c.SuccessRedirectURL == "" && c.FailureRedirectURL != "") { return fmt.Errorf("when using success and failure redirect URLs, set both URLs") } + if c.NonInteractivePromptText == "" { + c.NonInteractivePromptText = "Please enter a valid authorization code flow code: " + } if c.Logf == nil { c.Logf = func(string, ...interface{}) {} } @@ -138,18 +146,17 @@ func (c *Config) validateAndSetDefaults() error { // // This performs the following steps: // -// 1. Start a local server at the port. -// 2. Open a browser and navigate it to the local server. -// 3. Wait for the user authorization. -// 4. Receive a code via an authorization response (HTTP redirect). -// 5. Exchange the code and a token. -// 6. Return the code. -// +// 1. Start a local server at the port. +// 2. Open a browser and navigate it to the local server. +// 3. Wait for the user authorization. +// 4. Receive a code via an authorization response (HTTP redirect). +// 5. Exchange the code and a token. +// 6. Return the code. func GetToken(ctx context.Context, c Config) (*oauth2.Token, error) { if err := c.validateAndSetDefaults(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } - code, err := receiveCodeViaLocalServer(ctx, &c) + code, err := GetCode(ctx, c) if err != nil { return nil, fmt.Errorf("authorization error: %w", err) } @@ -160,3 +167,16 @@ func GetToken(ctx context.Context, c Config) (*oauth2.Token, error) { } return token, nil } + +func GetCode(ctx context.Context, c Config) (string, error) { + var code string + var err error + + if c.NonInteractive { + code, err = receiveCodeViaUserInput(&c) + } else { + code, err = receiveCodeViaLocalServer(ctx, &c) + } + + return code, err +} From 40503f273262687908af4256b60d5ca3b3007c14 Mon Sep 17 00:00:00 2001 From: Robin Trietsch Date: Fri, 9 Dec 2022 11:04:22 +0100 Subject: [PATCH 2/5] chore: module name change for development --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 97d196a..e8172c0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/int128/oauth2cli +module github.com/trietsch/oauth2cli require ( github.com/google/go-cmp v0.5.9 From e043e67a705d27ab15bc18be7f2cbaff4a99afd4 Mon Sep 17 00:00:00 2001 From: Robin Trietsch Date: Fri, 9 Dec 2022 12:35:56 +0100 Subject: [PATCH 3/5] fix: incorrect import --- oauth2cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2cli.go b/oauth2cli.go index c013b39..fe44a3d 100644 --- a/oauth2cli.go +++ b/oauth2cli.go @@ -5,9 +5,9 @@ package oauth2cli import ( "context" "fmt" + "github.com/trietsch/oauth2cli/oauth2params" "net/http" - "github.com/int128/oauth2cli/oauth2params" "golang.org/x/oauth2" ) From 60ae8470cec71012b9c5d7dce10796e98a322cc3 Mon Sep 17 00:00:00 2001 From: Robin Trietsch Date: Fri, 9 Dec 2022 23:10:10 +0100 Subject: [PATCH 4/5] feat: add support for headless machines / detached login process --- go.mod | 2 +- go.sum | 2 ++ non_interactive.go | 24 ++++++++++++++++---- oauth2cli.go | 55 +++++++++++++++++++++++++++++++++++++--------- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index e8172c0..97d196a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/trietsch/oauth2cli +module github.com/int128/oauth2cli require ( github.com/google/go-cmp v0.5.9 diff --git a/go.sum b/go.sum index ff5a244..eb4d1a1 100644 --- a/go.sum +++ b/go.sum @@ -55,3 +55,5 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/non_interactive.go b/non_interactive.go index ed99915..ac5b9cc 100644 --- a/non_interactive.go +++ b/non_interactive.go @@ -2,13 +2,16 @@ package oauth2cli import ( "bufio" + "encoding/base64" + "encoding/json" "fmt" "golang.org/x/sync/errgroup" "os" + "strings" ) -func receiveCodeViaUserInput(c *Config) (string, error) { - var userInput string +func receiveCodeViaUserInput(c *Config) (*OAuth2ConfigAndCode, error) { + var userInput *OAuth2ConfigAndCode var eg errgroup.Group @@ -19,13 +22,26 @@ func receiveCodeViaUserInput(c *Config) (string, error) { if err != nil { return err } else { - userInput = string(input) + cleanedInput := strings.TrimSuffix(string(input), "\n") + decoded, err := base64.StdEncoding.DecodeString(cleanedInput) + if err != nil { + return err + } + + configAndCode := OAuth2ConfigAndCode{} + err = json.Unmarshal(decoded, &configAndCode) + if err != nil { + return err + } + + userInput = &configAndCode + return nil } }) if err := eg.Wait(); err != nil { - return "", fmt.Errorf("non-interactive authorization error: %w", err) + return nil, err } return userInput, nil diff --git a/oauth2cli.go b/oauth2cli.go index fe44a3d..795e33b 100644 --- a/oauth2cli.go +++ b/oauth2cli.go @@ -4,8 +4,10 @@ package oauth2cli import ( "context" + "encoding/base64" + "encoding/json" "fmt" - "github.com/trietsch/oauth2cli/oauth2params" + "github.com/int128/oauth2cli/oauth2params" "net/http" "golang.org/x/oauth2" @@ -156,7 +158,23 @@ func GetToken(ctx context.Context, c Config) (*oauth2.Token, error) { if err := c.validateAndSetDefaults(); err != nil { return nil, fmt.Errorf("invalid config: %w", err) } - code, err := GetCode(ctx, c) + var code string + var err error + + if c.NonInteractive { + var codeAndConfig *OAuth2ConfigAndCode + codeAndConfig, err = receiveCodeViaUserInput(&c) + + if err != nil { + return nil, fmt.Errorf("error parsing user input: %w", err) + } + + code = (*codeAndConfig).Code + c.OAuth2Config = (*codeAndConfig).OAuth2Config + } else { + code, err = receiveCodeViaLocalServer(ctx, &c) + } + if err != nil { return nil, fmt.Errorf("authorization error: %w", err) } @@ -168,15 +186,32 @@ func GetToken(ctx context.Context, c Config) (*oauth2.Token, error) { return token, nil } -func GetCode(ctx context.Context, c Config) (string, error) { - var code string - var err error +type OAuth2ConfigAndCode struct { + OAuth2Config oauth2.Config + Code string +} - if c.NonInteractive { - code, err = receiveCodeViaUserInput(&c) - } else { - code, err = receiveCodeViaLocalServer(ctx, &c) +// GetCodeAndConfig cuts the authorization code flow in half. This allows for the +// login process to be performed on headless machines that do not have access to a +// browser by doing the interactive login on a machine that does have access to +// a browser, and copying the result onto the headless machine. +// The response of this function is a JSON that includes both the used +// OAuth2Config and the authorization code, as a base64 encoded string. +// This is the same string that receiveCodeViaUserInput expects +func GetCodeAndConfig(ctx context.Context, c Config) (*string, error) { + if err := c.validateAndSetDefaults(); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) + } + + code, err := receiveCodeViaLocalServer(ctx, &c) + + configAndCode := OAuth2ConfigAndCode{ + OAuth2Config: c.OAuth2Config, + Code: code, } - return code, err + bytes, err := json.Marshal(configAndCode) + jsonCodeAndConfig := base64.StdEncoding.EncodeToString(bytes) + + return &jsonCodeAndConfig, err } From 719c91aded522d582a558eef30e729fa3d0a1dfc Mon Sep 17 00:00:00 2001 From: Robin Trietsch Date: Sat, 10 Dec 2022 10:46:05 +0100 Subject: [PATCH 5/5] chore: fix golangci-lint error --- oauth2cli.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/oauth2cli.go b/oauth2cli.go index 795e33b..86cd241 100644 --- a/oauth2cli.go +++ b/oauth2cli.go @@ -204,14 +204,21 @@ func GetCodeAndConfig(ctx context.Context, c Config) (*string, error) { } code, err := receiveCodeViaLocalServer(ctx, &c) + if err != nil { + return nil, err + } configAndCode := OAuth2ConfigAndCode{ OAuth2Config: c.OAuth2Config, Code: code, } - bytes, err := json.Marshal(configAndCode) + bytes, unmarshalErr := json.Marshal(configAndCode) jsonCodeAndConfig := base64.StdEncoding.EncodeToString(bytes) - return &jsonCodeAndConfig, err + if unmarshalErr != nil { + return nil, unmarshalErr + } + + return &jsonCodeAndConfig, nil }