From 8e1432dedd67cc967ce9fe6d354da4865b62076b Mon Sep 17 00:00:00 2001 From: filip Date: Thu, 29 Aug 2024 14:03:04 +0200 Subject: [PATCH 01/16] Initial work on login command (not yet working) --- go.mod | 4 + go.sum | 10 +++ internal/cmd/login/login.go | 39 ++++++++++ internal/cmd/root/root.go | 2 + internal/login/login.go | 136 +++++++++++++++++++++++++++++++++ internal/wire/cli_container.go | 11 +++ 6 files changed, 202 insertions(+) create mode 100644 internal/cmd/login/login.go create mode 100644 internal/login/login.go diff --git a/go.mod b/go.mod index 378a24ce..a3c432ba 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/spf13/viper v1.15.0 github.com/stretchr/testify v1.8.4 github.com/vifraa/gopom v0.2.1 + golang.org/x/oauth2 v0.22.0 golang.org/x/tools v0.19.0 gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.2.1 @@ -25,12 +26,15 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/cli/browser v1.0.0 // indirect + github.com/cli/safeexec v1.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-oauth2/oauth2 v3.9.2+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index 4af1bbf0..7a44fbc6 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,12 @@ github.com/chelnak/ysmrr v0.2.1/go.mod h1:9TEgLy2xDMGN62zJm9XZrEWY/fHoGoBslSVEkE github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/browser v1.0.0 h1:RIleZgXrhdiCVgFBSjtWwkLPUCWyhhhN5k5HGSBt1js= +github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= +github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA= +github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= +github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= @@ -99,6 +105,8 @@ github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lK github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-oauth2/oauth2 v3.9.2+incompatible h1:A8gSjq4110EgZDVk4ZtcpusynU2Fto9eM6sXvxL+EOs= +github.com/go-oauth2/oauth2 v3.9.2+incompatible/go.mod h1:GGcZ+i513KxN4yS7zBYfmwo3P+cyGvCS675uCNmWv/g= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -375,6 +383,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/internal/cmd/login/login.go b/internal/cmd/login/login.go new file mode 100644 index 00000000..c2b26b48 --- /dev/null +++ b/internal/cmd/login/login.go @@ -0,0 +1,39 @@ +package login + +import ( + "fmt" + "github.com/debricked/cli/internal/login" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewLoginCmd(authenticator login.IAuthenticator) *cobra.Command { + cmd := &cobra.Command{ + Use: "login", + Short: "Authenticate debricked user", + Long: `Start authentication flow to generate access token.`, + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlags(cmd.Flags()) + }, + RunE: RunE(authenticator), + } + + return cmd +} + +func RunE(a login.IAuthenticator) func(_ *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { + token, err := a.Authenticate() + if err != nil { + return err + } + fmt.Printf( + "%s Successfully authenticated\nToken=%s", + color.GreenString("✔"), + color.BlueString(token), + ) + + return nil + } +} diff --git a/internal/cmd/root/root.go b/internal/cmd/root/root.go index ba36b774..6e9efee6 100644 --- a/internal/cmd/root/root.go +++ b/internal/cmd/root/root.go @@ -4,6 +4,7 @@ import ( "github.com/debricked/cli/internal/cmd/callgraph" "github.com/debricked/cli/internal/cmd/files" "github.com/debricked/cli/internal/cmd/fingerprint" + "github.com/debricked/cli/internal/cmd/login" "github.com/debricked/cli/internal/cmd/report" "github.com/debricked/cli/internal/cmd/resolve" "github.com/debricked/cli/internal/cmd/scan" @@ -47,6 +48,7 @@ Read more: https://docs.debricked.com/product/administration/generate-access-tok rootCmd.AddCommand(fingerprint.NewFingerprintCmd(container.Fingerprinter())) rootCmd.AddCommand(resolve.NewResolveCmd(container.Resolver())) rootCmd.AddCommand(callgraph.NewCallgraphCmd(container.CallgraphGenerator())) + rootCmd.AddCommand(login.NewLoginCmd(container.Authenticator())) rootCmd.CompletionOptions.DisableDefaultCmd = true diff --git a/internal/login/login.go b/internal/login/login.go new file mode 100644 index 00000000..4c77bab3 --- /dev/null +++ b/internal/login/login.go @@ -0,0 +1,136 @@ +package login + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + "math/rand" + "net/http" + "os/exec" + "runtime" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type IAuthenticator interface { + Authenticate() (string, error) +} + +type Authenticator struct { + ClientID string + Scopes []string +} + +const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +func generateRandomString(length int) string { + seed := rand.NewSource(time.Now().UnixNano()) + r := rand.New(seed) + + b := make([]byte, length) + for i := range b { + b[i] = charset[r.Intn(len(charset))] + } + return string(b) +} + +func createCodeChallenge(codeVerifier string) string { + // Create a SHA-256 hash of the code verifier + hash := sha256.Sum256([]byte(codeVerifier)) + + // Encode the hash to base64 + encoded := base64.StdEncoding.EncodeToString(hash[:]) + + // Make it URL safe + encoded = strings.TrimRight(encoded, "=") + encoded = strings.ReplaceAll(encoded, "+", "-") + encoded = strings.ReplaceAll(encoded, "/", "_") + + return encoded +} + +func (a Authenticator) Authenticate() (string, error) { + // Set up OAuth2 configuration + config := &oauth2.Config{ + ClientID: a.ClientID, + ClientSecret: "", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://debricked.com/app/oauth/authorize", + TokenURL: "https://debricked.com/app/oauth/token", + }, + RedirectURL: "http://localhost:9096/callback", + Scopes: a.Scopes, + } + + // Create a random state + state := generateRandomString(8) + codeVerifier := generateRandomString(64) + + // Generate the authorization URL + authURL := config.AuthCodeURL( + state, + oauth2.SetAuthURLParam("code_challenge", createCodeChallenge(codeVerifier)), + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + ) + + // Start a temporary HTTP server to handle the callback + code := make(chan string) + defer close(code) + server := &http.Server{Addr: ":9096"} + // Start the server in a goroutine + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + // Ensure the server is shut down when we're done + defer server.Shutdown(context.Background()) + + // Open the browser for the user to log in + err := openBrowser(authURL) + if err != nil { + log.Fatal("Could not open browser:", err) + } + + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("state") != state { + http.Error(w, "Invalid state", http.StatusBadRequest) + return + } + + code <- r.URL.Query().Get("code") + fmt.Fprintf(w, "Authentication successful! You can close this window now.") + }) + + // Wait for the authorization code + authCode := <-code + + // Exchange the authorization code for a token + token, err := config.Exchange(context.Background(), authCode) + if err != nil { + return "", err + } + return token.AccessToken, nil +} + +func openBrowser(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} diff --git a/internal/wire/cli_container.go b/internal/wire/cli_container.go index 9fbe1d33..c5a8a193 100644 --- a/internal/wire/cli_container.go +++ b/internal/wire/cli_container.go @@ -10,6 +10,7 @@ import ( "github.com/debricked/cli/internal/file" "github.com/debricked/cli/internal/fingerprint" "github.com/debricked/cli/internal/io" + "github.com/debricked/cli/internal/login" licenseReport "github.com/debricked/cli/internal/report/license" vulnerabilityReport "github.com/debricked/cli/internal/report/vulnerability" "github.com/debricked/cli/internal/resolution" @@ -92,6 +93,11 @@ func (cc *CliContainer) wire() error { cc.licenseReporter = licenseReport.Reporter{DebClient: cc.debClient} cc.vulnerabilityReporter = vulnerabilityReport.Reporter{DebClient: cc.debClient} + cc.authenticator = login.Authenticator{ + ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", + Scopes: []string{"select", "profile", "basicRepo"}, + } + return nil } @@ -112,6 +118,7 @@ type CliContainer struct { callgraph callgraph.IGenerator cgScheduler callgraph.IScheduler cgStrategyFactory callgraphStrategy.IFactory + authenticator login.IAuthenticator } func (cc *CliContainer) DebClient() client.IDebClient { @@ -146,6 +153,10 @@ func (cc *CliContainer) Fingerprinter() fingerprint.IFingerprint { return cc.fingerprinter } +func (cc *CliContainer) Authenticator() login.IAuthenticator { + return cc.authenticator +} + func wireErr(err error) error { return fmt.Errorf("failed to wire with cli-container. Error %s", err) } From 6252ecd576e04a7f811bb2f0f4b7c322f76f6c37 Mon Sep 17 00:00:00 2001 From: filip Date: Thu, 5 Sep 2024 16:05:53 +0200 Subject: [PATCH 02/16] Saving token in secret manager works --- go.mod | 6 +- go.sum | 8 ++ internal/cmd/login/login.go | 2 +- internal/login/auth.go | 141 ++++++++++++++++++++++++++++++++++++ internal/login/login.go | 132 +-------------------------------- internal/login/token.go | 67 +++++++++++++++++ 6 files changed, 225 insertions(+), 131 deletions(-) create mode 100644 internal/login/auth.go create mode 100644 internal/login/token.go diff --git a/go.mod b/go.mod index a3c432ba..9ce7bae9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/debricked/cli go 1.20 require ( + github.com/becheran/wildmatch-go v1.0.0 github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/chelnak/ysmrr v0.2.1 github.com/fatih/color v1.16.0 @@ -19,22 +20,24 @@ require ( golang.org/x/tools v0.19.0 gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.2.1 - github.com/becheran/wildmatch-go v1.0.0 ) require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/cli/browser v1.0.0 // indirect github.com/cli/safeexec v1.0.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-oauth2/oauth2 v3.9.2+incompatible // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -61,6 +64,7 @@ require ( github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zalando/go-keyring v0.2.5 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.22.0 // indirect diff --git a/go.sum b/go.sum index 7a44fbc6..8fc98695 100644 --- a/go.sum +++ b/go.sum @@ -45,6 +45,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= @@ -75,6 +77,8 @@ github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnht github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -107,6 +111,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-oauth2/oauth2 v3.9.2+incompatible h1:A8gSjq4110EgZDVk4ZtcpusynU2Fto9eM6sXvxL+EOs= github.com/go-oauth2/oauth2 v3.9.2+incompatible/go.mod h1:GGcZ+i513KxN4yS7zBYfmwo3P+cyGvCS675uCNmWv/g= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -280,6 +286,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= +github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/internal/cmd/login/login.go b/internal/cmd/login/login.go index c2b26b48..cd9d4426 100644 --- a/internal/cmd/login/login.go +++ b/internal/cmd/login/login.go @@ -24,7 +24,7 @@ func NewLoginCmd(authenticator login.IAuthenticator) *cobra.Command { func RunE(a login.IAuthenticator) func(_ *cobra.Command, args []string) error { return func(cmd *cobra.Command, _ []string) error { - token, err := a.Authenticate() + token, err := login.AuthToken() if err != nil { return err } diff --git a/internal/login/auth.go b/internal/login/auth.go new file mode 100644 index 00000000..52312d64 --- /dev/null +++ b/internal/login/auth.go @@ -0,0 +1,141 @@ +package login + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + // "github.com/debricked/cli/internal/client" + "log" + "math/rand" + "net/http" + "os/exec" + "runtime" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type IAuthenticator interface { + Authenticate() (*oauth2.Token, error) +} + +type Authenticator struct { + ClientID string + Scopes []string +} + +const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +func generateRandomString(length int) string { + seed := rand.NewSource(time.Now().UnixNano()) + r := rand.New(seed) + + b := make([]byte, length) + for i := range b { + b[i] = charset[r.Intn(len(charset))] + } + return string(b) +} + +func createCodeChallenge(codeVerifier string) string { + // Create a SHA-256 hash of the code verifier + hash := sha256.Sum256([]byte(codeVerifier)) + + // Encode the hash to base64 + encoded := base64.StdEncoding.EncodeToString(hash[:]) + + // Make it URL safe + encoded = strings.TrimRight(encoded, "=") + encoded = strings.ReplaceAll(encoded, "+", "-") + encoded = strings.ReplaceAll(encoded, "/", "_") + + return encoded +} + +func (a Authenticator) Authenticate() (*oauth2.Token, error) { + // Set up OAuth2 configuration + config := &oauth2.Config{ + ClientID: a.ClientID, + ClientSecret: "", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://debricked.com/app/oauth/authorize", + TokenURL: "https://debricked.com/app/oauth/token", + }, + RedirectURL: "http://localhost:9096/callback", + Scopes: a.Scopes, + } + + // Create a random state + state := generateRandomString(8) + codeVerifier := generateRandomString(64) + + // Generate the authorization URL + authURL := config.AuthCodeURL( + state, + oauth2.SetAuthURLParam("code_challenge", createCodeChallenge(codeVerifier)), + oauth2.SetAuthURLParam("code_challenge_method", "S256"), + ) + + // Start a temporary HTTP server to handle the callback + code := make(chan string) + defer close(code) + server := &http.Server{Addr: ":9096"} + // Start the server in a goroutine + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + // Ensure the server is shut down when we're done + defer server.Shutdown(context.Background()) + + // Open the browser for the user to log in + err := openBrowser(authURL) + if err != nil { + log.Fatal("Could not open browser:", err) + } + + http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("state") != state { + http.Error(w, "Invalid state", http.StatusBadRequest) + return + } + + code <- r.URL.Query().Get("code") + fmt.Fprintf(w, "Authentication successful! You can close this window now.") + }) + // Wait for the authorization code + authCode := <-code + + // Exchange the authorization code for a token + token, err := config.Exchange( + context.Background(), + authCode, + oauth2.SetAuthURLParam("client_id", a.ClientID), + oauth2.SetAuthURLParam("code_verifier", codeVerifier), + ) + if err != nil { + return nil, err + } + return token, nil +} + +func openBrowser(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +} diff --git a/internal/login/login.go b/internal/login/login.go index 4c77bab3..0d75fa1d 100644 --- a/internal/login/login.go +++ b/internal/login/login.go @@ -1,136 +1,10 @@ package login -import ( - "context" - "crypto/sha256" - "encoding/base64" - "fmt" - "log" - "math/rand" - "net/http" - "os/exec" - "runtime" - "strings" - "time" - - "golang.org/x/oauth2" -) - -type IAuthenticator interface { - Authenticate() (string, error) -} - -type Authenticator struct { - ClientID string - Scopes []string -} - -const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - -func generateRandomString(length int) string { - seed := rand.NewSource(time.Now().UnixNano()) - r := rand.New(seed) - - b := make([]byte, length) - for i := range b { - b[i] = charset[r.Intn(len(charset))] - } - return string(b) -} - -func createCodeChallenge(codeVerifier string) string { - // Create a SHA-256 hash of the code verifier - hash := sha256.Sum256([]byte(codeVerifier)) - - // Encode the hash to base64 - encoded := base64.StdEncoding.EncodeToString(hash[:]) - - // Make it URL safe - encoded = strings.TrimRight(encoded, "=") - encoded = strings.ReplaceAll(encoded, "+", "-") - encoded = strings.ReplaceAll(encoded, "/", "_") - - return encoded -} - -func (a Authenticator) Authenticate() (string, error) { - // Set up OAuth2 configuration - config := &oauth2.Config{ - ClientID: a.ClientID, - ClientSecret: "", - Endpoint: oauth2.Endpoint{ - AuthURL: "https://debricked.com/app/oauth/authorize", - TokenURL: "https://debricked.com/app/oauth/token", - }, - RedirectURL: "http://localhost:9096/callback", - Scopes: a.Scopes, - } - - // Create a random state - state := generateRandomString(8) - codeVerifier := generateRandomString(64) - - // Generate the authorization URL - authURL := config.AuthCodeURL( - state, - oauth2.SetAuthURLParam("code_challenge", createCodeChallenge(codeVerifier)), - oauth2.SetAuthURLParam("code_challenge_method", "S256"), - ) - - // Start a temporary HTTP server to handle the callback - code := make(chan string) - defer close(code) - server := &http.Server{Addr: ":9096"} - // Start the server in a goroutine - go func() { - if err := server.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("HTTP server error: %v", err) - } - }() - - // Ensure the server is shut down when we're done - defer server.Shutdown(context.Background()) - - // Open the browser for the user to log in - err := openBrowser(authURL) - if err != nil { - log.Fatal("Could not open browser:", err) - } - - http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("state") != state { - http.Error(w, "Invalid state", http.StatusBadRequest) - return - } - - code <- r.URL.Query().Get("code") - fmt.Fprintf(w, "Authentication successful! You can close this window now.") - }) - - // Wait for the authorization code - authCode := <-code - - // Exchange the authorization code for a token - token, err := config.Exchange(context.Background(), authCode) +func AuthToken() (string, error) { + tokenSource := GetDebrickedTokenSource() + token, err := tokenSource.Token() if err != nil { return "", err } return token.AccessToken, nil } - -func openBrowser(url string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "windows": - cmd = "cmd" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url) - return exec.Command(cmd, args...).Start() -} diff --git a/internal/login/token.go b/internal/login/token.go new file mode 100644 index 00000000..494fde82 --- /dev/null +++ b/internal/login/token.go @@ -0,0 +1,67 @@ +package login + +import ( + "github.com/zalando/go-keyring" + + "golang.org/x/oauth2" +) + +type SecretClient interface { + Set(string, string) error + Get(string) (string, error) +} + +type DebrickedSecretClient struct { + User string +} + +type DebrickedTokenSource struct { + SecretClient SecretClient +} + +func (dsc DebrickedSecretClient) Set(service, secret string) error { + return keyring.Set(service, dsc.User, secret) +} + +func (dsc DebrickedSecretClient) Get(service string) (string, error) { + return keyring.Get(service, dsc.User) +} + +func GetDebrickedTokenSource() oauth2.TokenSource { + return DebrickedTokenSource{ + SecretClient: DebrickedSecretClient{ + User: "DebrickedCLI", + }, + } +} + +func (dts DebrickedTokenSource) Token() (*oauth2.Token, error) { + refreshToken, err := dts.SecretClient.Get("DebrickedRefreshToken") + if err != nil { + if err == keyring.ErrNotFound { + // refreshToken is not yet set, initialize authorization + authenticator := Authenticator{ + ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", + Scopes: []string{"select", "profile", "basicRepo"}, + } + token, err := authenticator.Authenticate() + if err != nil { + return nil, err + } + dts.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) + dts.SecretClient.Set("DebrickedAccessToken", token.AccessToken) + } else { + return nil, err + } + } + accessToken, err := dts.SecretClient.Get("DebrickedAccessToken") + if err != nil { + accessToken = "" + } + // TODO: Parse expiry date + return &oauth2.Token{ + RefreshToken: refreshToken, + TokenType: "jwt", + AccessToken: accessToken, + }, nil +} From b4c39f5063317fd91663e7bd57b67c2477e5cae5 Mon Sep 17 00:00:00 2001 From: filip-debricked <135233582+filip-debricked@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:17:07 +0200 Subject: [PATCH 03/16] Apply suggestions from code review Use library functionality (and remove home-brewed alternatives) Co-authored-by: Linus Karlsson <197840+zozs@users.noreply.github.com> --- internal/login/auth.go | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/internal/login/auth.go b/internal/login/auth.go index 52312d64..b1abb817 100644 --- a/internal/login/auth.go +++ b/internal/login/auth.go @@ -39,21 +39,6 @@ func generateRandomString(length int) string { return string(b) } -func createCodeChallenge(codeVerifier string) string { - // Create a SHA-256 hash of the code verifier - hash := sha256.Sum256([]byte(codeVerifier)) - - // Encode the hash to base64 - encoded := base64.StdEncoding.EncodeToString(hash[:]) - - // Make it URL safe - encoded = strings.TrimRight(encoded, "=") - encoded = strings.ReplaceAll(encoded, "+", "-") - encoded = strings.ReplaceAll(encoded, "/", "_") - - return encoded -} - func (a Authenticator) Authenticate() (*oauth2.Token, error) { // Set up OAuth2 configuration config := &oauth2.Config{ @@ -68,14 +53,13 @@ func (a Authenticator) Authenticate() (*oauth2.Token, error) { } // Create a random state - state := generateRandomString(8) - codeVerifier := generateRandomString(64) + state := oauth2.GenerateVerifier() + codeVerifier := oauth2.GenerateVerifier() // Generate the authorization URL authURL := config.AuthCodeURL( state, - oauth2.SetAuthURLParam("code_challenge", createCodeChallenge(codeVerifier)), - oauth2.SetAuthURLParam("code_challenge_method", "S256"), + oauth2.S256ChallengeOption(codeVerifier), ) // Start a temporary HTTP server to handle the callback @@ -115,7 +99,7 @@ func (a Authenticator) Authenticate() (*oauth2.Token, error) { context.Background(), authCode, oauth2.SetAuthURLParam("client_id", a.ClientID), - oauth2.SetAuthURLParam("code_verifier", codeVerifier), + oauth2.VerifierOption(codeVerifier), ) if err != nil { return nil, err From fe278c00605cc8c8adc8e9bba6ae83b890272ff9 Mon Sep 17 00:00:00 2001 From: filip Date: Fri, 6 Sep 2024 13:17:58 +0200 Subject: [PATCH 04/16] remove token printout --- internal/cmd/login/login.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/cmd/login/login.go b/internal/cmd/login/login.go index cd9d4426..b63f85ff 100644 --- a/internal/cmd/login/login.go +++ b/internal/cmd/login/login.go @@ -24,14 +24,13 @@ func NewLoginCmd(authenticator login.IAuthenticator) *cobra.Command { func RunE(a login.IAuthenticator) func(_ *cobra.Command, args []string) error { return func(cmd *cobra.Command, _ []string) error { - token, err := login.AuthToken() + _, err := login.AuthToken() if err != nil { return err } fmt.Printf( - "%s Successfully authenticated\nToken=%s", + "%s Successfully authenticated", color.GreenString("✔"), - color.BlueString(token), ) return nil From d2ddc13a0431808e886e241dd42e99bb81e541ca Mon Sep 17 00:00:00 2001 From: filip Date: Mon, 9 Sep 2024 12:55:32 +0200 Subject: [PATCH 05/16] Rename and add subcommands --- internal/{login => auth}/auth.go | 93 ++++++++++++++++----- internal/client/deb_client.go | 5 ++ internal/client/testdata/deb_client_mock.go | 4 + internal/cmd/auth/auth.go | 26 ++++++ internal/cmd/{ => auth}/login/login.go | 8 +- internal/cmd/auth/logout/logout.go | 38 +++++++++ internal/cmd/auth/token/token.go | 40 +++++++++ internal/cmd/root/root.go | 4 +- internal/cmd/root/root_test.go | 8 +- internal/file/finder_test.go | 4 + internal/login/login.go | 10 --- internal/login/token.go | 67 --------------- internal/upload/uploader_test.go | 4 + internal/wire/cli_container.go | 12 +-- 14 files changed, 207 insertions(+), 116 deletions(-) rename internal/{login => auth}/auth.go (52%) create mode 100644 internal/cmd/auth/auth.go rename internal/cmd/{ => auth}/login/login.go (72%) create mode 100644 internal/cmd/auth/logout/logout.go create mode 100644 internal/cmd/auth/token/token.go delete mode 100644 internal/login/login.go delete mode 100644 internal/login/token.go diff --git a/internal/login/auth.go b/internal/auth/auth.go similarity index 52% rename from internal/login/auth.go rename to internal/auth/auth.go index b1abb817..444937c3 100644 --- a/internal/login/auth.go +++ b/internal/auth/auth.go @@ -1,52 +1,97 @@ -package login +package auth import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" - // "github.com/debricked/cli/internal/client" + "github.com/debricked/cli/internal/client" + "github.com/zalando/go-keyring" "log" - "math/rand" "net/http" "os/exec" "runtime" - "strings" - "time" "golang.org/x/oauth2" ) type IAuthenticator interface { - Authenticate() (*oauth2.Token, error) + Authenticate() error + Logout() error + Token() (*oauth2.Token, error) } type Authenticator struct { - ClientID string - Scopes []string + ClientID string + Scopes []string + Client client.IDebClient + SecretClient ISecretClient } -const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" +type ISecretClient interface { + Set(string, string) error + Get(string) (string, error) + Delete(string) error +} + +type DebrickedSecretClient struct { + User string +} + +func (dsc DebrickedSecretClient) Set(service, secret string) error { + return keyring.Set(service, dsc.User, secret) +} + +func (dsc DebrickedSecretClient) Get(service string) (string, error) { + return keyring.Get(service, dsc.User) +} + +func (dsc DebrickedSecretClient) Delete(service string) error { + return keyring.Delete(service, dsc.User) +} + +func NewDebrickedAuthenticator(client client.IDebClient) Authenticator { + return Authenticator{ + ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", + Scopes: []string{"select", "profile", "basicRepo"}, + Client: client, + SecretClient: DebrickedSecretClient{ + User: "DebrickedCLI", + }, + } +} -func generateRandomString(length int) string { - seed := rand.NewSource(time.Now().UnixNano()) - r := rand.New(seed) +func (a Authenticator) Logout() error { + err := a.SecretClient.Delete("DebrickedRefreshToken") + if err != nil { + return err + } + err = a.SecretClient.Delete("DebrickedAccessToken") + return err +} - b := make([]byte, length) - for i := range b { - b[i] = charset[r.Intn(len(charset))] +func (a Authenticator) Token() (*oauth2.Token, error) { + refreshToken, err := a.SecretClient.Get("DebrickedRefreshToken") + if err != nil { + return nil, err } - return string(b) + accessToken, err := a.SecretClient.Get("DebrickedAccessToken") + if err != nil { + return nil, err + } + return &oauth2.Token{ + RefreshToken: refreshToken, + TokenType: "jwt", + AccessToken: accessToken, + }, nil } -func (a Authenticator) Authenticate() (*oauth2.Token, error) { +func (a Authenticator) Authenticate() error { // Set up OAuth2 configuration config := &oauth2.Config{ ClientID: a.ClientID, ClientSecret: "", Endpoint: oauth2.Endpoint{ - AuthURL: "https://debricked.com/app/oauth/authorize", - TokenURL: "https://debricked.com/app/oauth/token", + AuthURL: a.Client.Host() + "/app/oauth/authorize", + TokenURL: a.Client.Host() + "/app/oauth/token", }, RedirectURL: "http://localhost:9096/callback", Scopes: a.Scopes, @@ -102,9 +147,11 @@ func (a Authenticator) Authenticate() (*oauth2.Token, error) { oauth2.VerifierOption(codeVerifier), ) if err != nil { - return nil, err + return err } - return token, nil + a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) + a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) + return nil } func openBrowser(url string) error { diff --git a/internal/client/deb_client.go b/internal/client/deb_client.go index 0a61f6af..dc98bbd7 100644 --- a/internal/client/deb_client.go +++ b/internal/client/deb_client.go @@ -22,6 +22,7 @@ type IDebClient interface { Get(uri string, format string) (*http.Response, error) SetAccessToken(accessToken *string) IsEnterpriseCustomer(silent bool) bool + Host() string } type DebClient struct { @@ -45,6 +46,10 @@ func NewDebClient(accessToken *string, httpClient IClient) *DebClient { } } +func (debClient *DebClient) Host() string { + return *debClient.host +} + func (debClient *DebClient) Post(uri string, contentType string, body *bytes.Buffer, timeout int) (*http.Response, error) { if timeout > 0 { return postWithTimeout(uri, debClient, contentType, body, true, timeout) diff --git a/internal/client/testdata/deb_client_mock.go b/internal/client/testdata/deb_client_mock.go index d897e3db..60feb184 100644 --- a/internal/client/testdata/deb_client_mock.go +++ b/internal/client/testdata/deb_client_mock.go @@ -34,6 +34,10 @@ func NewDebClientMock() *DebClientMock { } } +func (mock *DebClientMock) Host() string { + return "debricked.com" +} + func (mock *DebClientMock) Get(uri string, format string) (*http.Response, error) { response, err := mock.popResponse(mock.RemoveQueryParamsFromUri(uri)) diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go new file mode 100644 index 00000000..6dea5f34 --- /dev/null +++ b/internal/cmd/auth/auth.go @@ -0,0 +1,26 @@ +package auth + +import ( + "github.com/debricked/cli/internal/auth" + "github.com/debricked/cli/internal/cmd/auth/login" + "github.com/debricked/cli/internal/cmd/auth/logout" + "github.com/debricked/cli/internal/cmd/auth/token" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewAuthCmd(authenticator auth.IAuthenticator) *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Short: "Debricked authentication.", + Long: `Debricked service authentication.`, + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlags(cmd.Flags()) + }, + } + cmd.AddCommand(login.NewLoginCmd(authenticator)) + cmd.AddCommand(logout.NewLogoutCmd(authenticator)) + cmd.AddCommand(token.NewTokenCmd(authenticator)) + + return cmd +} diff --git a/internal/cmd/login/login.go b/internal/cmd/auth/login/login.go similarity index 72% rename from internal/cmd/login/login.go rename to internal/cmd/auth/login/login.go index b63f85ff..427a5204 100644 --- a/internal/cmd/login/login.go +++ b/internal/cmd/auth/login/login.go @@ -2,13 +2,13 @@ package login import ( "fmt" - "github.com/debricked/cli/internal/login" + "github.com/debricked/cli/internal/auth" "github.com/fatih/color" "github.com/spf13/cobra" "github.com/spf13/viper" ) -func NewLoginCmd(authenticator login.IAuthenticator) *cobra.Command { +func NewLoginCmd(authenticator auth.IAuthenticator) *cobra.Command { cmd := &cobra.Command{ Use: "login", Short: "Authenticate debricked user", @@ -22,9 +22,9 @@ func NewLoginCmd(authenticator login.IAuthenticator) *cobra.Command { return cmd } -func RunE(a login.IAuthenticator) func(_ *cobra.Command, args []string) error { +func RunE(a auth.IAuthenticator) func(_ *cobra.Command, args []string) error { return func(cmd *cobra.Command, _ []string) error { - _, err := login.AuthToken() + _, err := a.Authenticate() if err != nil { return err } diff --git a/internal/cmd/auth/logout/logout.go b/internal/cmd/auth/logout/logout.go new file mode 100644 index 00000000..c693c79f --- /dev/null +++ b/internal/cmd/auth/logout/logout.go @@ -0,0 +1,38 @@ +package logout + +import ( + "fmt" + "github.com/debricked/cli/internal/auth" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewLogoutCmd(authenticator auth.IAuthenticator) *cobra.Command { + cmd := &cobra.Command{ + Use: "logout", + Short: "Logout debricked user", + Long: `Remove cached credentials to logout debricked user.`, + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlags(cmd.Flags()) + }, + RunE: RunE(authenticator), + } + + return cmd +} + +func RunE(a auth.IAuthenticator) func(_ *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { + err := a.Logout() + if err != nil { + return err + } + fmt.Printf( + "%s Successfully removed credentials", + color.GreenString("✔"), + ) + + return nil + } +} diff --git a/internal/cmd/auth/token/token.go b/internal/cmd/auth/token/token.go new file mode 100644 index 00000000..68175e7a --- /dev/null +++ b/internal/cmd/auth/token/token.go @@ -0,0 +1,40 @@ +package token + +import ( + "fmt" + "github.com/debricked/cli/internal/auth" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func NewTokenCmd(authenticator auth.IAuthenticator) *cobra.Command { + cmd := &cobra.Command{ + Use: "token", + Short: "Retrieve access token", + Long: `Retrieve access token for currently logged in Debricked user.`, + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlags(cmd.Flags()) + }, + RunE: RunE(authenticator), + } + + return cmd +} + +func RunE(a auth.IAuthenticator) func(_ *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { + token, err := a.Token() + if err != nil { + return err + } + fmt.Printf( + "Refresh Token = %s\nAccess Token = %s\n", + color.BlueString(token.RefreshToken), + color.BlueString(token.AccessToken), + ) + + return nil + } +} diff --git a/internal/cmd/root/root.go b/internal/cmd/root/root.go index 6e9efee6..a98177f3 100644 --- a/internal/cmd/root/root.go +++ b/internal/cmd/root/root.go @@ -1,10 +1,10 @@ package root import ( + "github.com/debricked/cli/internal/cmd/auth" "github.com/debricked/cli/internal/cmd/callgraph" "github.com/debricked/cli/internal/cmd/files" "github.com/debricked/cli/internal/cmd/fingerprint" - "github.com/debricked/cli/internal/cmd/login" "github.com/debricked/cli/internal/cmd/report" "github.com/debricked/cli/internal/cmd/resolve" "github.com/debricked/cli/internal/cmd/scan" @@ -48,7 +48,7 @@ Read more: https://docs.debricked.com/product/administration/generate-access-tok rootCmd.AddCommand(fingerprint.NewFingerprintCmd(container.Fingerprinter())) rootCmd.AddCommand(resolve.NewResolveCmd(container.Resolver())) rootCmd.AddCommand(callgraph.NewCallgraphCmd(container.CallgraphGenerator())) - rootCmd.AddCommand(login.NewLoginCmd(container.Authenticator())) + rootCmd.AddCommand(auth.NewAuthCmd(container.Authenticator())) rootCmd.CompletionOptions.DisableDefaultCmd = true diff --git a/internal/cmd/root/root_test.go b/internal/cmd/root/root_test.go index a9a49731..cb6ce145 100644 --- a/internal/cmd/root/root_test.go +++ b/internal/cmd/root/root_test.go @@ -11,9 +11,13 @@ import ( func TestNewRootCmd(t *testing.T) { cmd := NewRootCmd("v0.0.0", wire.GetCliContainer()) commands := cmd.Commands() - nbrOfCommands := 6 + nbrOfCommands := 7 if len(commands) != nbrOfCommands { - t.Errorf("failed to assert that there were %d sub commands connected", nbrOfCommands) + t.Errorf( + "failed to assert that there were %d sub commands connected (was %d)", + nbrOfCommands, + len(commands), + ) } flags := cmd.PersistentFlags() diff --git a/internal/file/finder_test.go b/internal/file/finder_test.go index 991caa90..381e9d87 100644 --- a/internal/file/finder_test.go +++ b/internal/file/finder_test.go @@ -20,6 +20,10 @@ import ( type debClientMock struct{} +func (mock *debClientMock) Host() string { + return "debricked.com" +} + func (mock *debClientMock) Post(_ string, _ string, _ *bytes.Buffer, _ int) (*http.Response, error) { return &http.Response{}, nil } diff --git a/internal/login/login.go b/internal/login/login.go deleted file mode 100644 index 0d75fa1d..00000000 --- a/internal/login/login.go +++ /dev/null @@ -1,10 +0,0 @@ -package login - -func AuthToken() (string, error) { - tokenSource := GetDebrickedTokenSource() - token, err := tokenSource.Token() - if err != nil { - return "", err - } - return token.AccessToken, nil -} diff --git a/internal/login/token.go b/internal/login/token.go deleted file mode 100644 index 494fde82..00000000 --- a/internal/login/token.go +++ /dev/null @@ -1,67 +0,0 @@ -package login - -import ( - "github.com/zalando/go-keyring" - - "golang.org/x/oauth2" -) - -type SecretClient interface { - Set(string, string) error - Get(string) (string, error) -} - -type DebrickedSecretClient struct { - User string -} - -type DebrickedTokenSource struct { - SecretClient SecretClient -} - -func (dsc DebrickedSecretClient) Set(service, secret string) error { - return keyring.Set(service, dsc.User, secret) -} - -func (dsc DebrickedSecretClient) Get(service string) (string, error) { - return keyring.Get(service, dsc.User) -} - -func GetDebrickedTokenSource() oauth2.TokenSource { - return DebrickedTokenSource{ - SecretClient: DebrickedSecretClient{ - User: "DebrickedCLI", - }, - } -} - -func (dts DebrickedTokenSource) Token() (*oauth2.Token, error) { - refreshToken, err := dts.SecretClient.Get("DebrickedRefreshToken") - if err != nil { - if err == keyring.ErrNotFound { - // refreshToken is not yet set, initialize authorization - authenticator := Authenticator{ - ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", - Scopes: []string{"select", "profile", "basicRepo"}, - } - token, err := authenticator.Authenticate() - if err != nil { - return nil, err - } - dts.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) - dts.SecretClient.Set("DebrickedAccessToken", token.AccessToken) - } else { - return nil, err - } - } - accessToken, err := dts.SecretClient.Get("DebrickedAccessToken") - if err != nil { - accessToken = "" - } - // TODO: Parse expiry date - return &oauth2.Token{ - RefreshToken: refreshToken, - TokenType: "jwt", - AccessToken: accessToken, - }, nil -} diff --git a/internal/upload/uploader_test.go b/internal/upload/uploader_test.go index 5d3fb970..22483ba4 100644 --- a/internal/upload/uploader_test.go +++ b/internal/upload/uploader_test.go @@ -90,6 +90,10 @@ func TestUploadPollingError(t *testing.T) { type debClientMock struct{} +func (mock *debClientMock) Host() string { + return "debricked.com" +} + func (mock *debClientMock) Post(uri string, _ string, _ *bytes.Buffer, _ int) (*http.Response, error) { res := &http.Response{ Status: "", diff --git a/internal/wire/cli_container.go b/internal/wire/cli_container.go index c5a8a193..f232f16a 100644 --- a/internal/wire/cli_container.go +++ b/internal/wire/cli_container.go @@ -3,6 +3,7 @@ package wire import ( "fmt" + "github.com/debricked/cli/internal/auth" "github.com/debricked/cli/internal/callgraph" callgraphStrategy "github.com/debricked/cli/internal/callgraph/strategy" "github.com/debricked/cli/internal/ci" @@ -10,7 +11,6 @@ import ( "github.com/debricked/cli/internal/file" "github.com/debricked/cli/internal/fingerprint" "github.com/debricked/cli/internal/io" - "github.com/debricked/cli/internal/login" licenseReport "github.com/debricked/cli/internal/report/license" vulnerabilityReport "github.com/debricked/cli/internal/report/vulnerability" "github.com/debricked/cli/internal/resolution" @@ -92,11 +92,7 @@ func (cc *CliContainer) wire() error { cc.licenseReporter = licenseReport.Reporter{DebClient: cc.debClient} cc.vulnerabilityReporter = vulnerabilityReport.Reporter{DebClient: cc.debClient} - - cc.authenticator = login.Authenticator{ - ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", - Scopes: []string{"select", "profile", "basicRepo"}, - } + cc.authenticator = auth.NewDebrickedAuthenticator(cc.debClient) return nil } @@ -118,7 +114,7 @@ type CliContainer struct { callgraph callgraph.IGenerator cgScheduler callgraph.IScheduler cgStrategyFactory callgraphStrategy.IFactory - authenticator login.IAuthenticator + authenticator auth.IAuthenticator } func (cc *CliContainer) DebClient() client.IDebClient { @@ -153,7 +149,7 @@ func (cc *CliContainer) Fingerprinter() fingerprint.IFingerprint { return cc.fingerprinter } -func (cc *CliContainer) Authenticator() login.IAuthenticator { +func (cc *CliContainer) Authenticator() auth.IAuthenticator { return cc.authenticator } From 23164f92ddc127a5a2e202653f5a158c56043c62 Mon Sep 17 00:00:00 2001 From: filip Date: Wed, 11 Sep 2024 11:54:50 +0200 Subject: [PATCH 06/16] Restructured auth --- internal/auth/auth.go | 89 +++++++++++++++----------------- internal/auth/auth_test.go | 13 +++++ internal/cmd/auth/auth_test.go | 24 +++++++++ internal/cmd/auth/login/login.go | 3 +- internal/cmd/auth/token/token.go | 33 ++++++++++-- 5 files changed, 109 insertions(+), 53 deletions(-) create mode 100644 internal/auth/auth_test.go create mode 100644 internal/cmd/auth/auth_test.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 444937c3..efb88c91 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -20,10 +20,8 @@ type IAuthenticator interface { } type Authenticator struct { - ClientID string - Scopes []string - Client client.IDebClient SecretClient ISecretClient + OAuthConfig *oauth2.Config } type ISecretClient interface { @@ -50,12 +48,19 @@ func (dsc DebrickedSecretClient) Delete(service string) error { func NewDebrickedAuthenticator(client client.IDebClient) Authenticator { return Authenticator{ - ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", - Scopes: []string{"select", "profile", "basicRepo"}, - Client: client, SecretClient: DebrickedSecretClient{ User: "DebrickedCLI", }, + OAuthConfig: &oauth2.Config{ + ClientID: "01919462-7d6e-78e8-aa24-ba779213c90f", + ClientSecret: "", + Endpoint: oauth2.Endpoint{ + AuthURL: client.Host() + "/app/oauth/authorize", + TokenURL: client.Host() + "/app/oauth/token", + }, + RedirectURL: "http://localhost:9096/callback", + Scopes: []string{"select", "profile", "basicRepo"}, + }, } } @@ -84,49 +89,19 @@ func (a Authenticator) Token() (*oauth2.Token, error) { }, nil } -func (a Authenticator) Authenticate() error { - // Set up OAuth2 configuration - config := &oauth2.Config{ - ClientID: a.ClientID, - ClientSecret: "", - Endpoint: oauth2.Endpoint{ - AuthURL: a.Client.Host() + "/app/oauth/authorize", - TokenURL: a.Client.Host() + "/app/oauth/token", - }, - RedirectURL: "http://localhost:9096/callback", - Scopes: a.Scopes, - } - - // Create a random state - state := oauth2.GenerateVerifier() - codeVerifier := oauth2.GenerateVerifier() - - // Generate the authorization URL - authURL := config.AuthCodeURL( - state, - oauth2.S256ChallengeOption(codeVerifier), - ) - - // Start a temporary HTTP server to handle the callback +func (a Authenticator) callback(state string) string { code := make(chan string) defer close(code) - server := &http.Server{Addr: ":9096"} - // Start the server in a goroutine + server := &http.Server{Addr: ":9096"} // Start the server in a goroutine go func() { if err := server.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("HTTP server error: %v", err) } }() - // Ensure the server is shut down when we're done - defer server.Shutdown(context.Background()) - - // Open the browser for the user to log in - err := openBrowser(authURL) - if err != nil { - log.Fatal("Could not open browser:", err) - } - + defer server.Shutdown( + context.Background(), + ) // Ensure the server is shut down when we're done http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("state") != state { http.Error(w, "Invalid state", http.StatusBadRequest) @@ -136,19 +111,41 @@ func (a Authenticator) Authenticate() error { code <- r.URL.Query().Get("code") fmt.Fprintf(w, "Authentication successful! You can close this window now.") }) - // Wait for the authorization code - authCode := <-code + authCode := <-code // Wait for the authorization code - // Exchange the authorization code for a token - token, err := config.Exchange( + return authCode +} + +func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, error) { + return a.OAuthConfig.Exchange( context.Background(), authCode, - oauth2.SetAuthURLParam("client_id", a.ClientID), + oauth2.SetAuthURLParam("client_id", a.OAuthConfig.ClientID), oauth2.VerifierOption(codeVerifier), ) + +} + +func (a Authenticator) Authenticate() error { + state := oauth2.GenerateVerifier() + codeVerifier := oauth2.GenerateVerifier() + + authURL := a.OAuthConfig.AuthCodeURL( + state, + oauth2.S256ChallengeOption(codeVerifier), + ) + + err := openBrowser(authURL) + if err != nil { + log.Fatal("Could not open browser:", err) + } + + authCode := a.callback(state) + token, err := a.exchange(authCode, codeVerifier) if err != nil { return err } + a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) return nil diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 00000000..a2c74afc --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,13 @@ +package auth + +import ( + "testing" + + "github.com/debricked/cli/internal/client/testdata" + "github.com/stretchr/testify/assert" +) + +func TestNewGeneration(t *testing.T) { + res := NewDebrickedAuthenticator(testdata.NewDebClientMock()) + assert.NotNil(t, res) +} diff --git a/internal/cmd/auth/auth_test.go b/internal/cmd/auth/auth_test.go new file mode 100644 index 00000000..4cb4114a --- /dev/null +++ b/internal/cmd/auth/auth_test.go @@ -0,0 +1,24 @@ +package auth + +import ( + "testing" + + "github.com/debricked/cli/internal/auth" + "github.com/debricked/cli/internal/client" + "github.com/stretchr/testify/assert" +) + +func TestNewFilesCmd(t *testing.T) { + token := "token" + deb_client := client.NewDebClient(&token, nil) + authenticator := auth.NewDebrickedAuthenticator(deb_client) + cmd := NewAuthCmd(authenticator) + commands := cmd.Commands() + nbrOfCommands := 3 + assert.Lenf(t, commands, nbrOfCommands, "failed to assert that there were %d sub commands connected", nbrOfCommands) +} + +func TestPreRun(t *testing.T) { + cmd := NewAuthCmd(nil) + cmd.PreRun(cmd, nil) +} diff --git a/internal/cmd/auth/login/login.go b/internal/cmd/auth/login/login.go index 427a5204..e87689d7 100644 --- a/internal/cmd/auth/login/login.go +++ b/internal/cmd/auth/login/login.go @@ -18,13 +18,12 @@ func NewLoginCmd(authenticator auth.IAuthenticator) *cobra.Command { }, RunE: RunE(authenticator), } - return cmd } func RunE(a auth.IAuthenticator) func(_ *cobra.Command, args []string) error { return func(cmd *cobra.Command, _ []string) error { - _, err := a.Authenticate() + err := a.Authenticate() if err != nil { return err } diff --git a/internal/cmd/auth/token/token.go b/internal/cmd/auth/token/token.go index 68175e7a..c4140fba 100644 --- a/internal/cmd/auth/token/token.go +++ b/internal/cmd/auth/token/token.go @@ -1,7 +1,9 @@ package token import ( + "encoding/json" "fmt" + "github.com/debricked/cli/internal/auth" "github.com/fatih/color" @@ -9,6 +11,10 @@ import ( "github.com/spf13/viper" ) +var jsonFormat bool + +const JsonFlag = "json" + func NewTokenCmd(authenticator auth.IAuthenticator) *cobra.Command { cmd := &cobra.Command{ Use: "token", @@ -19,6 +25,18 @@ func NewTokenCmd(authenticator auth.IAuthenticator) *cobra.Command { }, RunE: RunE(authenticator), } + cmd.Flags().BoolVarP(&jsonFormat, JsonFlag, "j", false, `Print files in JSON format +Format: +[ + { + "access_token": , + "token_type": "jwt", + "refresh_token": , + "expiry": , + }, +] +`) + viper.MustBindEnv(JsonFlag) return cmd } @@ -29,11 +47,16 @@ func RunE(a auth.IAuthenticator) func(_ *cobra.Command, args []string) error { if err != nil { return err } - fmt.Printf( - "Refresh Token = %s\nAccess Token = %s\n", - color.BlueString(token.RefreshToken), - color.BlueString(token.AccessToken), - ) + if viper.GetBool(JsonFlag) { + jsonToken, _ := json.Marshal(token) + fmt.Println(string(jsonToken)) + } else { + fmt.Printf( + "Refresh Token = %s\nAccess Token = %s\n", + color.BlueString(token.RefreshToken), + color.BlueString(token.AccessToken), + ) + } return nil } From 4a66939f06c878b4762d3fe7c8c5d52203770dfd Mon Sep 17 00:00:00 2001 From: filip Date: Wed, 11 Sep 2024 14:07:08 +0200 Subject: [PATCH 07/16] Adds initial testing --- internal/auth/auth.go | 87 +++++------------ internal/auth/auth_test.go | 103 +++++++++++++++++++- internal/auth/callback.go | 83 ++++++++++++++++ internal/auth/callback_test.go | 124 ++++++++++++++++++++++++ internal/auth/testdata/auth_mock.go | 82 ++++++++++++++++ internal/cmd/auth/auth_test.go | 2 +- internal/cmd/auth/login/login_test.go | 44 +++++++++ internal/cmd/auth/logout/logout_test.go | 44 +++++++++ internal/cmd/auth/token/token_test.go | 67 +++++++++++++ 9 files changed, 571 insertions(+), 65 deletions(-) create mode 100644 internal/auth/callback.go create mode 100644 internal/auth/callback_test.go create mode 100644 internal/auth/testdata/auth_mock.go create mode 100644 internal/cmd/auth/login/login_test.go create mode 100644 internal/cmd/auth/logout/logout_test.go create mode 100644 internal/cmd/auth/token/token_test.go diff --git a/internal/auth/auth.go b/internal/auth/auth.go index efb88c91..37c413a2 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,14 +2,8 @@ package auth import ( "context" - "fmt" "github.com/debricked/cli/internal/client" "github.com/zalando/go-keyring" - "log" - "net/http" - "os/exec" - "runtime" - "golang.org/x/oauth2" ) @@ -19,17 +13,23 @@ type IAuthenticator interface { Token() (*oauth2.Token, error) } -type Authenticator struct { - SecretClient ISecretClient - OAuthConfig *oauth2.Config -} - type ISecretClient interface { Set(string, string) error Get(string) (string, error) Delete(string) error } +type IOAuthConfig interface { + AuthCodeURL(string, ...oauth2.AuthCodeOption) string + Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) +} // Wrapping interface for config to enable mocking + +type Authenticator struct { + SecretClient ISecretClient + OAuthConfig IOAuthConfig + AuthWebHelper IAuthWebHelper +} + type DebrickedSecretClient struct { User string } @@ -61,6 +61,7 @@ func NewDebrickedAuthenticator(client client.IDebClient) Authenticator { RedirectURL: "http://localhost:9096/callback", Scopes: []string{"select", "profile", "basicRepo"}, }, + AuthWebHelper: NewAuthWebHelper(), } } @@ -82,6 +83,7 @@ func (a Authenticator) Token() (*oauth2.Token, error) { if err != nil { return nil, err } + return &oauth2.Token{ RefreshToken: refreshToken, TokenType: "jwt", @@ -89,38 +91,21 @@ func (a Authenticator) Token() (*oauth2.Token, error) { }, nil } -func (a Authenticator) callback(state string) string { - code := make(chan string) - defer close(code) - server := &http.Server{Addr: ":9096"} // Start the server in a goroutine - go func() { - if err := server.ListenAndServe(); err != http.ErrServerClosed { - log.Fatalf("HTTP server error: %v", err) - } - }() - - defer server.Shutdown( - context.Background(), - ) // Ensure the server is shut down when we're done - http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("state") != state { - http.Error(w, "Invalid state", http.StatusBadRequest) - return - } - - code <- r.URL.Query().Get("code") - fmt.Fprintf(w, "Authentication successful! You can close this window now.") - }) - authCode := <-code // Wait for the authorization code - - return authCode +func (a Authenticator) saveToken(token *oauth2.Token) error { + err := a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) + if err != nil { + return err + } + err = a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) + + return err } func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, error) { + return a.OAuthConfig.Exchange( context.Background(), authCode, - oauth2.SetAuthURLParam("client_id", a.OAuthConfig.ClientID), oauth2.VerifierOption(codeVerifier), ) @@ -129,41 +114,21 @@ func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, e func (a Authenticator) Authenticate() error { state := oauth2.GenerateVerifier() codeVerifier := oauth2.GenerateVerifier() - authURL := a.OAuthConfig.AuthCodeURL( state, oauth2.S256ChallengeOption(codeVerifier), ) - err := openBrowser(authURL) + err := a.AuthWebHelper.OpenURL(authURL) if err != nil { - log.Fatal("Could not open browser:", err) + return err } - authCode := a.callback(state) + authCode := a.AuthWebHelper.Callback(state) token, err := a.exchange(authCode, codeVerifier) if err != nil { return err } - a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) - a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) - return nil -} - -func openBrowser(url string) error { - var cmd string - var args []string - - switch runtime.GOOS { - case "windows": - cmd = "cmd" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url) - return exec.Command(cmd, args...).Start() + return a.saveToken(token) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index a2c74afc..31b20f96 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -3,11 +3,108 @@ package auth import ( "testing" - "github.com/debricked/cli/internal/client/testdata" + "github.com/debricked/cli/internal/auth/testdata" + clientTestdata "github.com/debricked/cli/internal/client/testdata" "github.com/stretchr/testify/assert" + "github.com/zalando/go-keyring" + "golang.org/x/oauth2" ) -func TestNewGeneration(t *testing.T) { - res := NewDebrickedAuthenticator(testdata.NewDebClientMock()) +func TestNewAuthenticator(t *testing.T) { + res := NewDebrickedAuthenticator(clientTestdata.NewDebClientMock()) assert.NotNil(t, res) } + +func TestSecretClientSet(t *testing.T) { + user := "TestDebrickedCLIUserSet" + service := "TestDebrickedCLIServiceSet" + secret := "TestDebrickedCLISecretSet" + dsc := DebrickedSecretClient{user} + _, err := keyring.Get(service, user) + assert.Error(t, err) + err = dsc.Set(service, secret) + assert.NoError(t, err) + savedSecret, err := keyring.Get(service, user) + assert.NoError(t, err) + err = keyring.Delete(service, user) + assert.NoError(t, err) + assert.Equal(t, secret, savedSecret) +} + +func TestSecretClientGet(t *testing.T) { + user := "TestDebrickedCLIUserGet" + service := "TestDebrickedCLIServiceGet" + secret := "TestDebrickedCLISecretGet" + dsc := DebrickedSecretClient{user} + err := keyring.Set(service, user, secret) + assert.NoError(t, err) + savedSecret, err := dsc.Get(service) + assert.NoError(t, err) + err = keyring.Delete(service, user) + assert.NoError(t, err) + assert.Equal(t, secret, savedSecret) +} + +func TestSecretClientDelete(t *testing.T) { + user := "TestDebrickedCLIUserDelete" + service := "TestDebrickedCLIServiceDelete" + secret := "TestDebrickedCLISecretDelete" + dsc := DebrickedSecretClient{user} + err := keyring.Set(service, user, secret) + assert.NoError(t, err) + savedSecret, err := keyring.Get(service, user) + assert.NoError(t, err) + assert.Equal(t, secret, savedSecret) + err = dsc.Delete(service) + assert.NoError(t, err) + _, err = keyring.Get(service, user) + assert.Error(t, err) +} + +func TestMockedLogout(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: nil, + } + err := authenticator.Logout() + + assert.NoError(t, err) +} + +func TestMockedSaveToken(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: nil, + } + token := &oauth2.Token{ + RefreshToken: "refreshToken", + AccessToken: "accessToken", + } + err := authenticator.saveToken(token) + + assert.NoError(t, err) +} + +func TestMockedToken(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: nil, + } + token, err := authenticator.Token() + + assert.NoError(t, err) + assert.Equal(t, token.TokenType, "jwt") + assert.Equal(t, token.RefreshToken, "token") + assert.Equal(t, token.AccessToken, "token") +} + +func TestMockedAuthenticate(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: testdata.MockOAuthConfig{}, + AuthWebHelper: testdata.MockAuthWebHelper{}, + } + err := authenticator.Authenticate() + + assert.NoError(t, err) +} diff --git a/internal/auth/callback.go b/internal/auth/callback.go new file mode 100644 index 00000000..40d99d4f --- /dev/null +++ b/internal/auth/callback.go @@ -0,0 +1,83 @@ +package auth + +import ( + "context" + "fmt" + "log" + "net/http" + "os/exec" + "runtime" + "time" +) + +type IAuthWebHelper interface { + Callback(string) string + OpenURL(string) error +} + +type AuthWebHelper struct { + ServeMux *http.ServeMux +} + +func NewAuthWebHelper() AuthWebHelper { + mux := http.NewServeMux() + return AuthWebHelper{ + ServeMux: mux, + } +} + +func (awh AuthWebHelper) Callback(state string) string { + code := make(chan string) + defer close(code) + + awh.ServeMux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("state") != state { + http.Error(w, "Invalid state", http.StatusBadRequest) + return + } + + code <- r.URL.Query().Get("code") + fmt.Fprintf(w, "Authentication successful! You can close this window now.") + }) + + server := &http.Server{ + Addr: ":9096", + ReadHeaderTimeout: time.Minute, + Handler: awh.ServeMux, + } + go func() { + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("HTTP server error: %v", err) + } + }() + + defer server.Shutdown( + context.Background(), + ) + authCode := <-code // Wait for the authorization code + + return authCode +} + +func (awh AuthWebHelper) openBrowserCmd(runtimeOS, url string) *exec.Cmd { + var cmd string + var args []string + switch runtimeOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + + return exec.Command(cmd, args...) +} + +func (awh AuthWebHelper) OpenURL(authURL string) error { + openCmd := awh.openBrowserCmd(runtime.GOOS, authURL) + + return openCmd.Start() +} diff --git a/internal/auth/callback_test.go b/internal/auth/callback_test.go new file mode 100644 index 00000000..e8915ae0 --- /dev/null +++ b/internal/auth/callback_test.go @@ -0,0 +1,124 @@ +package auth + +import ( + "context" + "fmt" + "net/http" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCallback(t *testing.T) { + awh := NewAuthWebHelper() + testState := "test_state" + + resultChan := make(chan string) + go func() { + result := awh.Callback(testState) + resultChan <- result + }() + + time.Sleep(100 * time.Millisecond) + + testCode := "test_code" + resp, err := http.Get(fmt.Sprintf("http://localhost:9096/callback?state=%s&code=%s", testState, testCode)) + if err != nil { + t.Fatalf("Failed to make callback request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status OK, got %v", resp.Status) + } + + select { + case result := <-resultChan: + if result != testCode { + t.Errorf("Expected code %s, got %s", testCode, result) + } + case <-time.After(5 * time.Second): + t.Fatal("Test timed out") + } +} + +func TestCallbackInvalidState(t *testing.T) { + awh := NewAuthWebHelper() + testState := "test_state" + + go func() { + awh.Callback(testState) + }() + + time.Sleep(100 * time.Millisecond) + + resp, err := http.Get("http://localhost:9096/callback?state=invalid_state&code=test_code") + if err != nil { + t.Fatalf("Failed to make callback request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusBadRequest { + t.Errorf("Expected status Bad Request, got %v", resp.Status) + } +} + +func TestCallbackServerError(t *testing.T) { + server := &http.Server{Addr: ":9096"} + go server.ListenAndServe() + defer server.Shutdown(context.Background()) + + awh := AuthWebHelper{} + testState := "test_state" + + resultChan := make(chan error) + go func() { + defer func() { + if r := recover(); r != nil { + resultChan <- fmt.Errorf("panic occurred: %v", r) + } else { + resultChan <- nil + } + }() + awh.Callback(testState) + }() + + select { + case err := <-resultChan: + if err == nil { + t.Error("Expected an error due to server already running, but got none") + } + case <-time.After(2 * time.Second): + t.Fatal("Test timed out") + } +} + +func TestOpenBrowserCmd(t *testing.T) { + a := NewAuthWebHelper() + cases := []struct { + runtimeOS string + expectedCmd *exec.Cmd + }{ + { + runtimeOS: "darwin", + expectedCmd: exec.Command("open", "url"), + }, + { + runtimeOS: "linux", + expectedCmd: exec.Command("xdg-open", "url"), + }, + { + runtimeOS: "windows", + expectedCmd: exec.Command("cmd", "/c", "start", "url"), + }, + } + + for _, c := range cases { + t.Run(c.runtimeOS, func(t *testing.T) { + authCmd := a.openBrowserCmd(c.runtimeOS, "url") + assert.Equal(t, c.expectedCmd.Args, authCmd.Args) + }) + } +} diff --git a/internal/auth/testdata/auth_mock.go b/internal/auth/testdata/auth_mock.go new file mode 100644 index 00000000..9e496e00 --- /dev/null +++ b/internal/auth/testdata/auth_mock.go @@ -0,0 +1,82 @@ +package testdata + +import ( + "context" + + "golang.org/x/oauth2" +) + +type MockSecretClient struct{} + +func (msc MockSecretClient) Set(service, secret string) error { + return nil +} + +func (msc MockSecretClient) Get(service string) (string, error) { + return "token", nil +} + +func (msc MockSecretClient) Delete(service string) error { + return nil +} + +type MockError struct{} + +func (me MockError) Error() string { + return "MockError!" +} + +type MockAuthenticator struct{} + +type ErrorMockAuthenticator struct{} + +type MockOAuthConfig struct{} + +type MockAuthWebHelper struct{} + +func (ma MockAuthenticator) Authenticate() error { + return nil +} + +func (ma MockAuthenticator) Logout() error { + return nil +} + +func (ma MockAuthenticator) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + RefreshToken: "refresh", + AccessToken: "access", + TokenType: "jwt", + }, nil +} + +func (ma ErrorMockAuthenticator) Authenticate() error { + return MockError{} +} + +func (ma ErrorMockAuthenticator) Logout() error { + return MockError{} +} + +func (ma ErrorMockAuthenticator) Token() (*oauth2.Token, error) { + return nil, MockError{} +} + +func (mawh MockAuthWebHelper) OpenURL(string) error { + return nil +} + +func (mawh MockAuthWebHelper) Callback(string) string { + return "callback" +} + +func (moc MockOAuthConfig) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: "accessToken", + RefreshToken: "accessToken", + }, nil +} + +func (moc MockOAuthConfig) AuthCodeURL(string, ...oauth2.AuthCodeOption) string { + return "localhost" +} diff --git a/internal/cmd/auth/auth_test.go b/internal/cmd/auth/auth_test.go index 4cb4114a..b10f08f6 100644 --- a/internal/cmd/auth/auth_test.go +++ b/internal/cmd/auth/auth_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewFilesCmd(t *testing.T) { +func TestNewAuthCmd(t *testing.T) { token := "token" deb_client := client.NewDebClient(&token, nil) authenticator := auth.NewDebrickedAuthenticator(deb_client) diff --git a/internal/cmd/auth/login/login_test.go b/internal/cmd/auth/login/login_test.go new file mode 100644 index 00000000..bba8f0cc --- /dev/null +++ b/internal/cmd/auth/login/login_test.go @@ -0,0 +1,44 @@ +package login + +import ( + "testing" + + "github.com/debricked/cli/internal/auth" + "github.com/debricked/cli/internal/auth/testdata" + "github.com/debricked/cli/internal/client" + "github.com/stretchr/testify/assert" +) + +func TestNewLoginCmd(t *testing.T) { + token := "token" + deb_client := client.NewDebClient(&token, nil) + authenticator := auth.NewDebrickedAuthenticator(deb_client) + cmd := NewLoginCmd(authenticator) + commands := cmd.Commands() + nbrOfCommands := 0 + assert.Len(t, commands, nbrOfCommands) +} + +func TestPreRun(t *testing.T) { + mockAuthenticator := testdata.MockAuthenticator{} + cmd := NewLoginCmd(mockAuthenticator) + cmd.PreRun(cmd, nil) +} + +func TestRunE(t *testing.T) { + a := testdata.MockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.NoError(t, err) +} + +func TestRunEError(t *testing.T) { + a := testdata.ErrorMockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.Error(t, err) +} diff --git a/internal/cmd/auth/logout/logout_test.go b/internal/cmd/auth/logout/logout_test.go new file mode 100644 index 00000000..5bb95f0e --- /dev/null +++ b/internal/cmd/auth/logout/logout_test.go @@ -0,0 +1,44 @@ +package logout + +import ( + "testing" + + "github.com/debricked/cli/internal/auth" + "github.com/debricked/cli/internal/auth/testdata" + "github.com/debricked/cli/internal/client" + "github.com/stretchr/testify/assert" +) + +func TestNewLogoutCmd(t *testing.T) { + token := "token" + deb_client := client.NewDebClient(&token, nil) + authenticator := auth.NewDebrickedAuthenticator(deb_client) + cmd := NewLogoutCmd(authenticator) + commands := cmd.Commands() + nbrOfCommands := 0 + assert.Len(t, commands, nbrOfCommands) +} + +func TestPreRun(t *testing.T) { + mockAuthenticator := testdata.MockAuthenticator{} + cmd := NewLogoutCmd(mockAuthenticator) + cmd.PreRun(cmd, nil) +} + +func TestRunE(t *testing.T) { + a := testdata.MockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.NoError(t, err) +} + +func TestRunEError(t *testing.T) { + a := testdata.ErrorMockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.Error(t, err) +} diff --git a/internal/cmd/auth/token/token_test.go b/internal/cmd/auth/token/token_test.go new file mode 100644 index 00000000..b684ef1f --- /dev/null +++ b/internal/cmd/auth/token/token_test.go @@ -0,0 +1,67 @@ +package token + +import ( + "testing" + + "github.com/debricked/cli/internal/auth" + "github.com/debricked/cli/internal/auth/testdata" + "github.com/debricked/cli/internal/client" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" +) + +func TestNewTokenCmd(t *testing.T) { + token := "token" + deb_client := client.NewDebClient(&token, nil) + authenticator := auth.NewDebrickedAuthenticator(deb_client) + cmd := NewTokenCmd(authenticator) + commands := cmd.Commands() + nbrOfCommands := 0 + assert.Len(t, commands, nbrOfCommands) + + flags := cmd.Flags() + flagAssertions := map[string]string{} + for name, shorthand := range flagAssertions { + flag := flags.Lookup(name) + assert.NotNil(t, flag) + assert.Equal(t, shorthand, flag.Shorthand) + } + + var flagKeys = []string{ + JsonFlag, + } + viperKeys := viper.AllKeys() + for _, flagKey := range flagKeys { + match := false + for _, key := range viperKeys { + if key == flagKey { + match = true + } + } + assert.Truef(t, match, "failed to assert that flag was present: "+flagKey) + } +} + +func TestPreRun(t *testing.T) { + mockAuthenticator := testdata.MockAuthenticator{} + cmd := NewTokenCmd(mockAuthenticator) + cmd.PreRun(cmd, nil) +} + +func TestRunE(t *testing.T) { + a := testdata.MockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.NoError(t, err) +} + +func TestRunEError(t *testing.T) { + a := testdata.ErrorMockAuthenticator{} + runE := RunE(a) + + err := runE(nil, []string{}) + + assert.Error(t, err) +} From 9ffb485432a33cfed088961e79c813ba5ca73c13 Mon Sep 17 00:00:00 2001 From: filip Date: Thu, 12 Sep 2024 19:51:49 +0200 Subject: [PATCH 08/16] Adds more tests --- internal/auth/auth.go | 9 ++--- internal/auth/auth_test.go | 63 +++++++++++++++++++++++++++++ internal/auth/callback_test.go | 5 +-- internal/auth/testdata/auth_mock.go | 46 +++++++++++++++++++-- 4 files changed, 112 insertions(+), 11 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 37c413a2..45eeb82f 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -22,7 +22,7 @@ type ISecretClient interface { type IOAuthConfig interface { AuthCodeURL(string, ...oauth2.AuthCodeOption) string Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) -} // Wrapping interface for config to enable mocking +} // Wrapping interface for config to simplify mocking type Authenticator struct { SecretClient ISecretClient @@ -70,8 +70,8 @@ func (a Authenticator) Logout() error { if err != nil { return err } - err = a.SecretClient.Delete("DebrickedAccessToken") - return err + + return a.SecretClient.Delete("DebrickedAccessToken") } func (a Authenticator) Token() (*oauth2.Token, error) { @@ -96,9 +96,8 @@ func (a Authenticator) saveToken(token *oauth2.Token) error { if err != nil { return err } - err = a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) - return err + return a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) } func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, error) { diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 31b20f96..9897ac27 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -71,6 +71,18 @@ func TestMockedLogout(t *testing.T) { assert.NoError(t, err) } +func TestMockedLogoutErrorRefresh(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockErrorSecretClient{ + ErrorPattern: "Refresh", + }, + OAuthConfig: nil, + } + err := authenticator.Logout() + + assert.Error(t, err) +} + func TestMockedSaveToken(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockSecretClient{}, @@ -85,6 +97,22 @@ func TestMockedSaveToken(t *testing.T) { assert.NoError(t, err) } +func TestMockedSaveTokenRefreshError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockErrorSecretClient{ + ErrorPattern: "Refresh", + }, + OAuthConfig: nil, + } + token := &oauth2.Token{ + RefreshToken: "refreshToken", + AccessToken: "accessToken", + } + err := authenticator.saveToken(token) + + assert.Error(t, err) +} + func TestMockedToken(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockSecretClient{}, @@ -98,6 +126,30 @@ func TestMockedToken(t *testing.T) { assert.Equal(t, token.AccessToken, "token") } +func TestMockedTokenRefreshError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockErrorSecretClient{ + ErrorPattern: "Refresh", + }, + OAuthConfig: nil, + } + _, err := authenticator.Token() + + assert.Error(t, err) +} + +func TestMockedTokenAccessError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockErrorSecretClient{ + ErrorPattern: "Access", + }, + OAuthConfig: nil, + } + _, err := authenticator.Token() + + assert.Error(t, err) +} + func TestMockedAuthenticate(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockSecretClient{}, @@ -108,3 +160,14 @@ func TestMockedAuthenticate(t *testing.T) { assert.NoError(t, err) } + +func TestMockedAuthenticateOpenURLError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: testdata.MockOAuthConfig{}, + AuthWebHelper: testdata.MockErrorAuthWebHelper{}, + } + err := authenticator.Authenticate() + + assert.Error(t, err) +} diff --git a/internal/auth/callback_test.go b/internal/auth/callback_test.go index e8915ae0..9723c3aa 100644 --- a/internal/auth/callback_test.go +++ b/internal/auth/callback_test.go @@ -11,9 +11,10 @@ import ( "github.com/stretchr/testify/assert" ) +const testState = "test_state" + func TestCallback(t *testing.T) { awh := NewAuthWebHelper() - testState := "test_state" resultChan := make(chan string) go func() { @@ -46,7 +47,6 @@ func TestCallback(t *testing.T) { func TestCallbackInvalidState(t *testing.T) { awh := NewAuthWebHelper() - testState := "test_state" go func() { awh.Callback(testState) @@ -71,7 +71,6 @@ func TestCallbackServerError(t *testing.T) { defer server.Shutdown(context.Background()) awh := AuthWebHelper{} - testState := "test_state" resultChan := make(chan error) go func() { diff --git a/internal/auth/testdata/auth_mock.go b/internal/auth/testdata/auth_mock.go index 9e496e00..c903e2b7 100644 --- a/internal/auth/testdata/auth_mock.go +++ b/internal/auth/testdata/auth_mock.go @@ -2,10 +2,17 @@ package testdata import ( "context" + "strings" "golang.org/x/oauth2" ) +type MockError struct{} + +func (me MockError) Error() string { + return "MockError!" +} + type MockSecretClient struct{} func (msc MockSecretClient) Set(service, secret string) error { @@ -20,10 +27,33 @@ func (msc MockSecretClient) Delete(service string) error { return nil } -type MockError struct{} +type MockErrorSecretClient struct { + ErrorPattern string +} -func (me MockError) Error() string { - return "MockError!" +func (msc MockErrorSecretClient) Set(service, secret string) error { + if strings.Contains(service, msc.ErrorPattern) { + + return MockError{} + } + + return nil +} + +func (msc MockErrorSecretClient) Get(service string) (string, error) { + if strings.Contains(service, msc.ErrorPattern) { + + return "", MockError{} + } + return "token", nil +} + +func (msc MockErrorSecretClient) Delete(service string) error { + if strings.Contains(service, msc.ErrorPattern) { + + return MockError{} + } + return nil } type MockAuthenticator struct{} @@ -34,6 +64,8 @@ type MockOAuthConfig struct{} type MockAuthWebHelper struct{} +type MockErrorAuthWebHelper struct{} + func (ma MockAuthenticator) Authenticate() error { return nil } @@ -70,6 +102,14 @@ func (mawh MockAuthWebHelper) Callback(string) string { return "callback" } +func (mawh MockErrorAuthWebHelper) OpenURL(string) error { + return MockError{} +} + +func (mawh MockErrorAuthWebHelper) Callback(string) string { + return "callback" +} + func (moc MockOAuthConfig) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) { return &oauth2.Token{ AccessToken: "accessToken", From 5df0f8d2849d184b345b228b9b0b7e7f6409e611 Mon Sep 17 00:00:00 2001 From: filip Date: Thu, 12 Sep 2024 20:05:58 +0200 Subject: [PATCH 09/16] Add secret client dependency --- build/docker/debian.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/docker/debian.Dockerfile b/build/docker/debian.Dockerfile index b88e8bca..25b83310 100644 --- a/build/docker/debian.Dockerfile +++ b/build/docker/debian.Dockerfile @@ -45,7 +45,7 @@ RUN echo "deb http://deb.debian.org/debian unstable main" >> /etc/apt/sources.li # echo "Pin: release a=testing" >> /etc/apt/preferences && \ # echo "Pin-Priority: -3" >> /etc/apt/preferences -RUN apt -y update && apt -y upgrade && apt -y install curl gnupg unzip && \ +RUN apt -y update && apt -y upgrade && apt -y install curl gnupg unzip gnome-keyring && \ apt -y clean && rm -rf /var/lib/apt/lists/* RUN mkdir -p /etc/apt/keyrings From 059efc8ad90132cc03e9f2bdc0ea184992b77135 Mon Sep 17 00:00:00 2001 From: filip Date: Fri, 13 Sep 2024 15:32:02 +0200 Subject: [PATCH 10/16] Add initial refresh --- go.mod | 1 + go.sum | 2 + internal/auth/auth.go | 48 ++++++++++++---- internal/auth/auth_test.go | 87 ++++++++++++++++++++++++++++- internal/auth/testdata/auth_mock.go | 87 ++++++++++++++++++++++++++--- internal/cmd/auth/token/token.go | 2 - 6 files changed, 202 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 9ce7bae9..cbce542b 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-oauth2/oauth2 v3.9.2+incompatible // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8fc98695..5958399c 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/go-oauth2/oauth2 v3.9.2+incompatible h1:A8gSjq4110EgZDVk4ZtcpusynU2Ft github.com/go-oauth2/oauth2 v3.9.2+incompatible/go.mod h1:GGcZ+i513KxN4yS7zBYfmwo3P+cyGvCS675uCNmWv/g= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 45eeb82f..40d076fe 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,7 +2,9 @@ package auth import ( "context" + "github.com/debricked/cli/internal/client" + "github.com/golang-jwt/jwt" "github.com/zalando/go-keyring" "golang.org/x/oauth2" ) @@ -22,6 +24,7 @@ type ISecretClient interface { type IOAuthConfig interface { AuthCodeURL(string, ...oauth2.AuthCodeOption) string Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) + TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource } // Wrapping interface for config to simplify mocking type Authenticator struct { @@ -74,6 +77,12 @@ func (a Authenticator) Logout() error { return a.SecretClient.Delete("DebrickedAccessToken") } +func validateJWT(token string) error { + claims := jwt.MapClaims{} + jwt.ParseWithClaims(token, claims, nil) + return claims.Valid() +} + func (a Authenticator) Token() (*oauth2.Token, error) { refreshToken, err := a.SecretClient.Get("DebrickedRefreshToken") if err != nil { @@ -83,15 +92,21 @@ func (a Authenticator) Token() (*oauth2.Token, error) { if err != nil { return nil, err } - + jwtErr := validateJWT(accessToken) + if jwtErr != nil { + if jwtErr.Error() == "Token is expired" { + return a.refresh(refreshToken) + } else { + return nil, jwtErr + } + } return &oauth2.Token{ RefreshToken: refreshToken, - TokenType: "jwt", AccessToken: accessToken, }, nil } -func (a Authenticator) saveToken(token *oauth2.Token) error { +func (a Authenticator) save(token *oauth2.Token) error { err := a.SecretClient.Set("DebrickedRefreshToken", token.RefreshToken) if err != nil { return err @@ -100,14 +115,21 @@ func (a Authenticator) saveToken(token *oauth2.Token) error { return a.SecretClient.Set("DebrickedAccessToken", token.AccessToken) } -func (a Authenticator) exchange(authCode, codeVerifier string) (*oauth2.Token, error) { - - return a.OAuthConfig.Exchange( +func (a Authenticator) refresh(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{ + RefreshToken: refreshToken, + } + tokenSource := a.OAuthConfig.TokenSource( context.Background(), - authCode, - oauth2.VerifierOption(codeVerifier), + token, ) - + token, err := tokenSource.Token() + if err != nil { + return nil, err + } else { + a.save(token) + return token, nil + } } func (a Authenticator) Authenticate() error { @@ -124,10 +146,14 @@ func (a Authenticator) Authenticate() error { } authCode := a.AuthWebHelper.Callback(state) - token, err := a.exchange(authCode, codeVerifier) + token, err := a.OAuthConfig.Exchange( + context.Background(), + authCode, + oauth2.VerifierOption(codeVerifier), + ) if err != nil { return err } - return a.saveToken(token) + return a.save(token) } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 9897ac27..0706ee22 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -45,6 +45,20 @@ func TestSecretClientGet(t *testing.T) { assert.Equal(t, secret, savedSecret) } +func TestSecretClientGetExpired(t *testing.T) { + user := "TestDebrickedCLIUserGet" + service := "TestDebrickedCLIServiceGet" + secret := "TestDebrickedCLISecretGet" + dsc := DebrickedSecretClient{user} + err := keyring.Set(service, user, secret) + assert.NoError(t, err) + savedSecret, err := dsc.Get(service) + assert.NoError(t, err) + err = keyring.Delete(service, user) + assert.NoError(t, err) + assert.Equal(t, secret, savedSecret) +} + func TestSecretClientDelete(t *testing.T) { user := "TestDebrickedCLIUserDelete" service := "TestDebrickedCLIServiceDelete" @@ -92,7 +106,7 @@ func TestMockedSaveToken(t *testing.T) { RefreshToken: "refreshToken", AccessToken: "accessToken", } - err := authenticator.saveToken(token) + err := authenticator.save(token) assert.NoError(t, err) } @@ -108,7 +122,7 @@ func TestMockedSaveTokenRefreshError(t *testing.T) { RefreshToken: "refreshToken", AccessToken: "accessToken", } - err := authenticator.saveToken(token) + err := authenticator.save(token) assert.Error(t, err) } @@ -121,11 +135,28 @@ func TestMockedToken(t *testing.T) { token, err := authenticator.Token() assert.NoError(t, err) - assert.Equal(t, token.TokenType, "jwt") assert.Equal(t, token.RefreshToken, "token") assert.Equal(t, token.AccessToken, "token") } +func TestMockedTokenExpired(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockExpiredSecretClient{}, + OAuthConfig: testdata.MockOAuthConfig{ + MockTokenSource: testdata.MockTokenSource{ + StaticToken: &oauth2.Token{ + RefreshToken: "refreshToken", + AccessToken: "accessToken", + }, + Error: nil, + }, + }, + } + _, err := authenticator.Token() + + assert.NoError(t, err) +} + func TestMockedTokenRefreshError(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockErrorSecretClient{ @@ -161,6 +192,56 @@ func TestMockedAuthenticate(t *testing.T) { assert.NoError(t, err) } +func TestMockedAuthenticateExchangeError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: testdata.MockOAuthConfigExchangeError{}, + AuthWebHelper: testdata.MockAuthWebHelper{}, + } + err := authenticator.Authenticate() + + assert.Error(t, err) + assert.Equal(t, "HTTP Error", err.Error()) +} + +func TestMockedRefresh(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: testdata.MockOAuthConfig{ + MockTokenSource: testdata.MockTokenSource{ + StaticToken: &oauth2.Token{ + RefreshToken: "refreshToken", + AccessToken: "accessToken", + }, + Error: nil, + }, + }, + AuthWebHelper: testdata.MockAuthWebHelper{}, + } + token, err := authenticator.refresh("refreshToken") + + assert.NoError(t, err) + assert.Equal(t, "accessToken", token.AccessToken) +} + +func TestMockedRefreshError(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockSecretClient{}, + OAuthConfig: testdata.MockOAuthConfig{ + MockTokenSource: testdata.MockTokenSource{ + StaticToken: nil, + Error: testdata.MockError{ + Message: "testerror", + }, + }, + }, + AuthWebHelper: testdata.MockAuthWebHelper{}, + } + _, err := authenticator.refresh("refreshToken") + + assert.Error(t, err) +} + func TestMockedAuthenticateOpenURLError(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockSecretClient{}, diff --git a/internal/auth/testdata/auth_mock.go b/internal/auth/testdata/auth_mock.go index c903e2b7..c3540025 100644 --- a/internal/auth/testdata/auth_mock.go +++ b/internal/auth/testdata/auth_mock.go @@ -7,14 +7,20 @@ import ( "golang.org/x/oauth2" ) -type MockError struct{} +type MockError struct { + Message string +} func (me MockError) Error() string { - return "MockError!" + return me.Message } type MockSecretClient struct{} +type MockExpiredSecretClient struct{} + +type MockInvalidSecretClient struct{} + func (msc MockSecretClient) Set(service, secret string) error { return nil } @@ -27,14 +33,41 @@ func (msc MockSecretClient) Delete(service string) error { return nil } +func (msc MockExpiredSecretClient) Set(service, secret string) error { + return nil +} + +func (msc MockExpiredSecretClient) Get(service string) (string, error) { + return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMTkxOTQ2Mi03ZDZlLTc4ZTgtYWEyNC1iYTc3OTIxM2M5MGYiLCJqdGkiOiJlMTdhMmFlYTk0ZjgyNTdjYWU1NWM3ZjRiNTczNTRiMzI2YmNiYTZiZmY3ZGQ0ZWQ2NjU3NDA4MWE4ODFjN2VhMmM3OGU3Y2EzM2UxMjU5MyIsImlhdCI6MTY5NDU5NzkzNy4zNjAwMTUsIm5iZiI6MTY5NDU5NzkzNy4zNjAwMTcsImV4cCI6MTY5NDU5NzkzNy4zNTM3MDMsInN1YiI6ImZpbGlwLmhlZGVuK2FkbWluQGRlYnJpY2tlZC5jb20iLCJzY29wZXMiOlsic2VsZWN0IiwicHJvZmlsZSIsImJhc2ljUmVwbyJdfQ.CMqnQM9QFHTthDMv4K8q6gmkkFmbOIhrmKXwfo7kMWU", nil +} + +func (msc MockExpiredSecretClient) Delete(service string) error { + return nil +} + +func (msc MockInvalidSecretClient) Set(service, secret string) error { + return nil +} + +func (msc MockInvalidSecretClient) Get(service string) (string, error) { + return "eyJhdWQiOiIwMTkxOTQ2Mi03ZDZlLTc4ZTgtYWEyNC1iYTc3OTIxM2M5MGYiLCJqdGkiOiJlMTdhMmFlYTk0ZjgyNTdjYWU1NWM3ZjRiNTczNTRiMzI2YmNiYTZiZmY3ZGQ0ZWQ2NjU3NDA4MWE4ODFjN2VhMmM3OGU3Y2EzM2UxMjU5MyIsImlhdCI6MTY5NDU5NzkzNy4zNjAwMTUsIm5iZiI6MTY5NDU5NzkzNy4zNjAwMTcsImV4cCI6MTY5NDU5NzkzNy4zNTM3MDMsInN1YiI6ImZpbGlwLmhlZGVuK2FkbWluQGRlYnJpY2tlZC5jb20iLCJzY29wZXMiOlsic2VsZWN0IiwicHJvZmlsZSIsImJhc2ljUmVwbyJdfQ", nil +} + +func (msc MockInvalidSecretClient) Delete(service string) error { + return nil +} + type MockErrorSecretClient struct { ErrorPattern string + Message string } func (msc MockErrorSecretClient) Set(service, secret string) error { if strings.Contains(service, msc.ErrorPattern) { - return MockError{} + return MockError{ + Message: msc.Message, + } } return nil @@ -43,7 +76,9 @@ func (msc MockErrorSecretClient) Set(service, secret string) error { func (msc MockErrorSecretClient) Get(service string) (string, error) { if strings.Contains(service, msc.ErrorPattern) { - return "", MockError{} + return "", MockError{ + Message: msc.Message, + } } return "token", nil } @@ -51,7 +86,9 @@ func (msc MockErrorSecretClient) Get(service string) (string, error) { func (msc MockErrorSecretClient) Delete(service string) error { if strings.Contains(service, msc.ErrorPattern) { - return MockError{} + return MockError{ + Message: msc.Message, + } } return nil } @@ -60,7 +97,11 @@ type MockAuthenticator struct{} type ErrorMockAuthenticator struct{} -type MockOAuthConfig struct{} +type MockOAuthConfig struct { + MockTokenSource oauth2.TokenSource +} + +type MockOAuthConfigExchangeError struct{} type MockAuthWebHelper struct{} @@ -83,15 +124,15 @@ func (ma MockAuthenticator) Token() (*oauth2.Token, error) { } func (ma ErrorMockAuthenticator) Authenticate() error { - return MockError{} + return MockError{""} } func (ma ErrorMockAuthenticator) Logout() error { - return MockError{} + return MockError{""} } func (ma ErrorMockAuthenticator) Token() (*oauth2.Token, error) { - return nil, MockError{} + return nil, MockError{""} } func (mawh MockAuthWebHelper) OpenURL(string) error { @@ -120,3 +161,31 @@ func (moc MockOAuthConfig) Exchange(context.Context, string, ...oauth2.AuthCodeO func (moc MockOAuthConfig) AuthCodeURL(string, ...oauth2.AuthCodeOption) string { return "localhost" } + +func (moc MockOAuthConfigExchangeError) Exchange(context.Context, string, ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + return nil, MockError{Message: "HTTP Error"} +} + +func (moc MockOAuthConfigExchangeError) AuthCodeURL(string, ...oauth2.AuthCodeOption) string { + return "localhost" +} + +func (moc MockOAuthConfigExchangeError) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { + return nil +} + +type MockTokenSource struct { + StaticToken *oauth2.Token + Error error +} + +func (mts MockTokenSource) Token() (*oauth2.Token, error) { + if mts.Error != nil { + return nil, mts.Error + } + return mts.StaticToken, nil +} + +func (moc MockOAuthConfig) TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource { + return moc.MockTokenSource +} diff --git a/internal/cmd/auth/token/token.go b/internal/cmd/auth/token/token.go index c4140fba..44b4aa48 100644 --- a/internal/cmd/auth/token/token.go +++ b/internal/cmd/auth/token/token.go @@ -30,9 +30,7 @@ Format: [ { "access_token": , - "token_type": "jwt", "refresh_token": , - "expiry": , }, ] `) From a0b3337a55c02c1898b05d005368a66e92670bbc Mon Sep 17 00:00:00 2001 From: filip Date: Fri, 13 Sep 2024 17:22:35 +0200 Subject: [PATCH 11/16] Add linux skip for secret client --- internal/auth/auth_test.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 0706ee22..210715df 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -1,6 +1,7 @@ package auth import ( + "runtime" "testing" "github.com/debricked/cli/internal/auth/testdata" @@ -10,12 +11,18 @@ import ( "golang.org/x/oauth2" ) +const windowsOS = "windows" +const macOS = "darwin" + func TestNewAuthenticator(t *testing.T) { res := NewDebrickedAuthenticator(clientTestdata.NewDebClientMock()) assert.NotNil(t, res) } func TestSecretClientSet(t *testing.T) { + if runtime.GOOS != windowsOS && runtime.GOOS != macOS { + t.Skipf("TestSecretClient is skipped due to env without secret client") + } user := "TestDebrickedCLIUserSet" service := "TestDebrickedCLIServiceSet" secret := "TestDebrickedCLISecretSet" @@ -32,20 +39,9 @@ func TestSecretClientSet(t *testing.T) { } func TestSecretClientGet(t *testing.T) { - user := "TestDebrickedCLIUserGet" - service := "TestDebrickedCLIServiceGet" - secret := "TestDebrickedCLISecretGet" - dsc := DebrickedSecretClient{user} - err := keyring.Set(service, user, secret) - assert.NoError(t, err) - savedSecret, err := dsc.Get(service) - assert.NoError(t, err) - err = keyring.Delete(service, user) - assert.NoError(t, err) - assert.Equal(t, secret, savedSecret) -} - -func TestSecretClientGetExpired(t *testing.T) { + if runtime.GOOS != windowsOS && runtime.GOOS != macOS { + t.Skipf("TestSecretClient is skipped due to env without secret client") + } user := "TestDebrickedCLIUserGet" service := "TestDebrickedCLIServiceGet" secret := "TestDebrickedCLISecretGet" @@ -60,6 +56,9 @@ func TestSecretClientGetExpired(t *testing.T) { } func TestSecretClientDelete(t *testing.T) { + if runtime.GOOS != windowsOS && runtime.GOOS != macOS { + t.Skipf("TestSecretClient is skipped due to env without secret client") + } user := "TestDebrickedCLIUserDelete" service := "TestDebrickedCLIServiceDelete" secret := "TestDebrickedCLISecretDelete" From 52370c0ba0cab8e4d785c4b691b2260a58cbc468 Mon Sep 17 00:00:00 2001 From: filip Date: Tue, 17 Sep 2024 08:51:54 +0200 Subject: [PATCH 12/16] Revert "Add secret client dependency" This reverts commit 5df0f8d2849d184b345b228b9b0b7e7f6409e611. --- build/docker/debian.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/docker/debian.Dockerfile b/build/docker/debian.Dockerfile index 25b83310..b88e8bca 100644 --- a/build/docker/debian.Dockerfile +++ b/build/docker/debian.Dockerfile @@ -45,7 +45,7 @@ RUN echo "deb http://deb.debian.org/debian unstable main" >> /etc/apt/sources.li # echo "Pin: release a=testing" >> /etc/apt/preferences && \ # echo "Pin-Priority: -3" >> /etc/apt/preferences -RUN apt -y update && apt -y upgrade && apt -y install curl gnupg unzip gnome-keyring && \ +RUN apt -y update && apt -y upgrade && apt -y install curl gnupg unzip && \ apt -y clean && rm -rf /var/lib/apt/lists/* RUN mkdir -p /etc/apt/keyrings From b2a8ceff346677a1ec396d9d5a45debd8981a753 Mon Sep 17 00:00:00 2001 From: filip Date: Tue, 17 Sep 2024 09:04:47 +0200 Subject: [PATCH 13/16] Escape & for windows cmd --- internal/auth/callback.go | 1 + internal/auth/callback_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 40d99d4f..16885031 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -65,6 +65,7 @@ func (awh AuthWebHelper) openBrowserCmd(runtimeOS, url string) *exec.Cmd { switch runtimeOS { case "windows": cmd = "cmd" + url = "\"" + url + "\"" // Windows does not like "&" args = []string{"/c", "start"} case "darwin": cmd = "open" diff --git a/internal/auth/callback_test.go b/internal/auth/callback_test.go index 9723c3aa..3eb8506d 100644 --- a/internal/auth/callback_test.go +++ b/internal/auth/callback_test.go @@ -110,7 +110,7 @@ func TestOpenBrowserCmd(t *testing.T) { }, { runtimeOS: "windows", - expectedCmd: exec.Command("cmd", "/c", "start", "url"), + expectedCmd: exec.Command("cmd", "/c", "start", "\"url\""), }, } From 8054b15757731193967e5733682cb9e089cd9c45 Mon Sep 17 00:00:00 2001 From: filip Date: Tue, 17 Sep 2024 09:48:42 +0200 Subject: [PATCH 14/16] Change to lib for opening browser --- go.mod | 1 + go.sum | 2 ++ internal/auth/callback.go | 25 ++----------------------- internal/auth/callback_test.go | 31 ------------------------------- 4 files changed, 5 insertions(+), 54 deletions(-) diff --git a/go.mod b/go.mod index cbce542b..21572310 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/sergi/go-diff v1.3.1 // indirect diff --git a/go.sum b/go.sum index 5958399c..ad105d80 100644 --- a/go.sum +++ b/go.sum @@ -229,6 +229,8 @@ github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9Cjg github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 16885031..3ce419a1 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -3,10 +3,9 @@ package auth import ( "context" "fmt" + "github.com/pkg/browser" "log" "net/http" - "os/exec" - "runtime" "time" ) @@ -59,26 +58,6 @@ func (awh AuthWebHelper) Callback(state string) string { return authCode } -func (awh AuthWebHelper) openBrowserCmd(runtimeOS, url string) *exec.Cmd { - var cmd string - var args []string - switch runtimeOS { - case "windows": - cmd = "cmd" - url = "\"" + url + "\"" // Windows does not like "&" - args = []string{"/c", "start"} - case "darwin": - cmd = "open" - default: // "linux", "freebsd", "openbsd", "netbsd" - cmd = "xdg-open" - } - args = append(args, url) - - return exec.Command(cmd, args...) -} - func (awh AuthWebHelper) OpenURL(authURL string) error { - openCmd := awh.openBrowserCmd(runtime.GOOS, authURL) - - return openCmd.Start() + return browser.OpenURL(authURL) } diff --git a/internal/auth/callback_test.go b/internal/auth/callback_test.go index 3eb8506d..09e124e4 100644 --- a/internal/auth/callback_test.go +++ b/internal/auth/callback_test.go @@ -4,11 +4,8 @@ import ( "context" "fmt" "net/http" - "os/exec" "testing" "time" - - "github.com/stretchr/testify/assert" ) const testState = "test_state" @@ -93,31 +90,3 @@ func TestCallbackServerError(t *testing.T) { t.Fatal("Test timed out") } } - -func TestOpenBrowserCmd(t *testing.T) { - a := NewAuthWebHelper() - cases := []struct { - runtimeOS string - expectedCmd *exec.Cmd - }{ - { - runtimeOS: "darwin", - expectedCmd: exec.Command("open", "url"), - }, - { - runtimeOS: "linux", - expectedCmd: exec.Command("xdg-open", "url"), - }, - { - runtimeOS: "windows", - expectedCmd: exec.Command("cmd", "/c", "start", "\"url\""), - }, - } - - for _, c := range cases { - t.Run(c.runtimeOS, func(t *testing.T) { - authCmd := a.openBrowserCmd(c.runtimeOS, "url") - assert.Equal(t, c.expectedCmd.Args, authCmd.Args) - }) - } -} From d6a68c1fb9487ef973314d26d8525855fb792275 Mon Sep 17 00:00:00 2001 From: filip Date: Wed, 18 Sep 2024 13:58:52 +0200 Subject: [PATCH 15/16] Lint fixes --- internal/auth/auth.go | 14 +++++++++++--- internal/auth/auth_test.go | 15 ++++++++++++--- internal/auth/callback.go | 14 +++++++++----- internal/auth/callback_test.go | 24 ++++++++++++++++-------- internal/auth/testdata/auth_mock.go | 24 ++++++++++++++++++++++-- internal/cmd/auth/login/login.go | 2 ++ internal/cmd/auth/logout/logout.go | 1 + 7 files changed, 73 insertions(+), 21 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 40d076fe..c31e59fd 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -2,6 +2,7 @@ package auth import ( "context" + "strings" "github.com/debricked/cli/internal/client" "github.com/golang-jwt/jwt" @@ -79,7 +80,12 @@ func (a Authenticator) Logout() error { func validateJWT(token string) error { claims := jwt.MapClaims{} - jwt.ParseWithClaims(token, claims, nil) + _, err := jwt.ParseWithClaims(token, claims, nil) + if err != nil && strings.Compare(err.Error(), "no Keyfunc was provided.") != 0 { + + return err + } + return claims.Valid() } @@ -100,6 +106,7 @@ func (a Authenticator) Token() (*oauth2.Token, error) { return nil, jwtErr } } + return &oauth2.Token{ RefreshToken: refreshToken, AccessToken: accessToken, @@ -127,8 +134,9 @@ func (a Authenticator) refresh(refreshToken string) (*oauth2.Token, error) { if err != nil { return nil, err } else { - a.save(token) - return token, nil + err = a.save(token) + + return token, err } } diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 210715df..53d9bdbb 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -126,16 +126,25 @@ func TestMockedSaveTokenRefreshError(t *testing.T) { assert.Error(t, err) } +func TestMockedTokenErrorSegments(t *testing.T) { + authenticator := Authenticator{ + SecretClient: testdata.MockInvalidSecretClient{}, + OAuthConfig: nil, + } + token, err := authenticator.Token() + assert.Error(t, err) + assert.ErrorContains(t, err, "token contains an invalid number of segments") + assert.Nil(t, token) +} + func TestMockedToken(t *testing.T) { authenticator := Authenticator{ SecretClient: testdata.MockSecretClient{}, OAuthConfig: nil, } token, err := authenticator.Token() - assert.NoError(t, err) - assert.Equal(t, token.RefreshToken, "token") - assert.Equal(t, token.AccessToken, "token") + assert.NotNil(t, token) } func TestMockedTokenExpired(t *testing.T) { diff --git a/internal/auth/callback.go b/internal/auth/callback.go index 3ce419a1..b918b30d 100644 --- a/internal/auth/callback.go +++ b/internal/auth/callback.go @@ -3,10 +3,11 @@ package auth import ( "context" "fmt" - "github.com/pkg/browser" "log" "net/http" "time" + + "github.com/pkg/browser" ) type IAuthWebHelper interface { @@ -20,6 +21,7 @@ type AuthWebHelper struct { func NewAuthWebHelper() AuthWebHelper { mux := http.NewServeMux() + return AuthWebHelper{ ServeMux: mux, } @@ -32,6 +34,7 @@ func (awh AuthWebHelper) Callback(state string) string { awh.ServeMux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("state") != state { http.Error(w, "Invalid state", http.StatusBadRequest) + return } @@ -49,10 +52,11 @@ func (awh AuthWebHelper) Callback(state string) string { log.Fatalf("HTTP server error: %v", err) } }() - - defer server.Shutdown( - context.Background(), - ) + defer func() { + if err := server.Shutdown(context.Background()); err != nil { + log.Fatalf("HTTP server shutdown error: %v", err) + } + }() authCode := <-code // Wait for the authorization code return authCode diff --git a/internal/auth/callback_test.go b/internal/auth/callback_test.go index 09e124e4..d62c1026 100644 --- a/internal/auth/callback_test.go +++ b/internal/auth/callback_test.go @@ -6,6 +6,8 @@ import ( "net/http" "testing" "time" + + "github.com/stretchr/testify/assert" ) const testState = "test_state" @@ -26,8 +28,10 @@ func TestCallback(t *testing.T) { if err != nil { t.Fatalf("Failed to make callback request: %v", err) } - defer resp.Body.Close() - + defer func() { + err := resp.Body.Close() + assert.NoError(t, err) + }() if resp.StatusCode != http.StatusOK { t.Errorf("Expected status OK, got %v", resp.Status) } @@ -63,9 +67,15 @@ func TestCallbackInvalidState(t *testing.T) { } func TestCallbackServerError(t *testing.T) { - server := &http.Server{Addr: ":9096"} - go server.ListenAndServe() - defer server.Shutdown(context.Background()) + server := &http.Server{Addr: ":9096", ReadHeaderTimeout: time.Second} + go func() { + err := server.ListenAndServe() + assert.Error(t, err) // Two servers trying to run on localhost:9096 + }() + defer func() { + err := server.Shutdown(context.Background()) + assert.NoError(t, err) + }() awh := AuthWebHelper{} @@ -83,9 +93,7 @@ func TestCallbackServerError(t *testing.T) { select { case err := <-resultChan: - if err == nil { - t.Error("Expected an error due to server already running, but got none") - } + assert.Error(t, err) case <-time.After(2 * time.Second): t.Fatal("Test timed out") } diff --git a/internal/auth/testdata/auth_mock.go b/internal/auth/testdata/auth_mock.go index c3540025..064352b7 100644 --- a/internal/auth/testdata/auth_mock.go +++ b/internal/auth/testdata/auth_mock.go @@ -3,7 +3,9 @@ package testdata import ( "context" "strings" + "time" + "github.com/golang-jwt/jwt" "golang.org/x/oauth2" ) @@ -26,7 +28,16 @@ func (msc MockSecretClient) Set(service, secret string) error { } func (msc MockSecretClient) Get(service string) (string, error) { - return "token", nil + var secretKey = []byte("secret-key") + token := jwt.NewWithClaims( + jwt.SigningMethodHS256, + jwt.MapClaims{ + "username": "mockDebrickedClient", + "exp": time.Now().Add(time.Hour).Unix(), + }, + ) + + return token.SignedString(secretKey) } func (msc MockSecretClient) Delete(service string) error { @@ -38,7 +49,16 @@ func (msc MockExpiredSecretClient) Set(service, secret string) error { } func (msc MockExpiredSecretClient) Get(service string) (string, error) { - return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMTkxOTQ2Mi03ZDZlLTc4ZTgtYWEyNC1iYTc3OTIxM2M5MGYiLCJqdGkiOiJlMTdhMmFlYTk0ZjgyNTdjYWU1NWM3ZjRiNTczNTRiMzI2YmNiYTZiZmY3ZGQ0ZWQ2NjU3NDA4MWE4ODFjN2VhMmM3OGU3Y2EzM2UxMjU5MyIsImlhdCI6MTY5NDU5NzkzNy4zNjAwMTUsIm5iZiI6MTY5NDU5NzkzNy4zNjAwMTcsImV4cCI6MTY5NDU5NzkzNy4zNTM3MDMsInN1YiI6ImZpbGlwLmhlZGVuK2FkbWluQGRlYnJpY2tlZC5jb20iLCJzY29wZXMiOlsic2VsZWN0IiwicHJvZmlsZSIsImJhc2ljUmVwbyJdfQ.CMqnQM9QFHTthDMv4K8q6gmkkFmbOIhrmKXwfo7kMWU", nil + var secretKey = []byte("secret-key") + token := jwt.NewWithClaims( + jwt.SigningMethodHS256, + jwt.MapClaims{ + "username": "mockDebrickedClient", + "exp": time.Now().Add(-time.Hour).Unix(), + }, + ) + + return token.SignedString(secretKey) } func (msc MockExpiredSecretClient) Delete(service string) error { diff --git a/internal/cmd/auth/login/login.go b/internal/cmd/auth/login/login.go index e87689d7..b2c697f2 100644 --- a/internal/cmd/auth/login/login.go +++ b/internal/cmd/auth/login/login.go @@ -2,6 +2,7 @@ package login import ( "fmt" + "github.com/debricked/cli/internal/auth" "github.com/fatih/color" "github.com/spf13/cobra" @@ -18,6 +19,7 @@ func NewLoginCmd(authenticator auth.IAuthenticator) *cobra.Command { }, RunE: RunE(authenticator), } + return cmd } diff --git a/internal/cmd/auth/logout/logout.go b/internal/cmd/auth/logout/logout.go index c693c79f..840af21f 100644 --- a/internal/cmd/auth/logout/logout.go +++ b/internal/cmd/auth/logout/logout.go @@ -2,6 +2,7 @@ package logout import ( "fmt" + "github.com/debricked/cli/internal/auth" "github.com/fatih/color" "github.com/spf13/cobra" From 7494f6c7ee0cadd6b42f358b05397d13677c34cf Mon Sep 17 00:00:00 2001 From: filip Date: Wed, 18 Sep 2024 15:06:30 +0200 Subject: [PATCH 16/16] Add beta feature warning --- internal/cmd/auth/auth.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go index 6dea5f34..8953ddf4 100644 --- a/internal/cmd/auth/auth.go +++ b/internal/cmd/auth/auth.go @@ -1,6 +1,8 @@ package auth import ( + "log" + "github.com/debricked/cli/internal/auth" "github.com/debricked/cli/internal/cmd/auth/login" "github.com/debricked/cli/internal/cmd/auth/logout" @@ -12,12 +14,13 @@ import ( func NewAuthCmd(authenticator auth.IAuthenticator) *cobra.Command { cmd := &cobra.Command{ Use: "auth", - Short: "Debricked authentication.", - Long: `Debricked service authentication.`, + Short: "Debricked authentication. [beta feature]", + Long: `Debricked service authentication. Currently in beta and will most likely not work as expected`, PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlags(cmd.Flags()) }, } + log.Println("Warning: Authentication is beta and may not work as expected.") cmd.AddCommand(login.NewLoginCmd(authenticator)) cmd.AddCommand(logout.NewLogoutCmd(authenticator)) cmd.AddCommand(token.NewTokenCmd(authenticator))