From 8e1432dedd67cc967ce9fe6d354da4865b62076b Mon Sep 17 00:00:00 2001 From: filip Date: Thu, 29 Aug 2024 14:03:04 +0200 Subject: [PATCH] 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) }