diff --git a/cmd/login.go b/cmd/login.go new file mode 100644 index 0000000..47b5823 --- /dev/null +++ b/cmd/login.go @@ -0,0 +1,84 @@ +package cmd + +import ( + "errors" + "fmt" + "net/http" + "os" + + magellan "github.com/OpenCHAMI/magellan/internal" + "github.com/lestrrat-go/jwx/jwt" + "github.com/spf13/cobra" +) + +var ( + loginUrl string + targetHost string + targetPort int + tokenPath string + forceLogin bool + noBrowser bool +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Log in with identity provider for access token", + Long: "", + Run: func(cmd *cobra.Command, args []string) { + // check if we have a valid JWT before starting login + if !forceLogin { + // try getting the access token from env var + testToken := []byte(os.Getenv("OCHAMI_ACCESS_TOKEN")) + if testToken == nil { + // try reading access token from a file + b, err := os.ReadFile(tokenPath) + if err != nil { + fmt.Printf("failed to read access token from file: %v\n", err) + return + } + testToken = b + } + // parse into jwt.Token to validate + token, err := jwt.Parse(testToken) + if err != nil { + fmt.Printf("failed to parse access token contents: %v\n", err) + return + } + // check if the token is invalid and we need a new one + err = jwt.Validate(token) + if err != nil { + fmt.Printf("failed to validate access token...fetching a new one") + } else { + fmt.Printf("found a valid token...skipping login (use the '-f/--force' flag to login anyway)") + return + } + } + + // start the login flow + var err error + accessToken, err = magellan.Login(loginUrl, targetHost, targetPort) + if errors.Is(err, http.ErrServerClosed) { + fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n") + } else if err != nil { + fmt.Printf("failed to start server: %v\n", err) + } + + // if we got a new token successfully, save it to the token path + if accessToken != "" && tokenPath != "" { + err := os.WriteFile(tokenPath, []byte(accessToken), os.ModePerm) + if err != nil { + fmt.Printf("failed to write access token to file: %v\n", err) + } + } + }, +} + +func init() { + loginCmd.Flags().StringVar(&loginUrl, "url", "http://127.0.0.1:3333/login", "set the login URL") + loginCmd.Flags().StringVar(&targetHost, "target-host", "127.0.0.1", "set the target host to return the access code") + loginCmd.Flags().IntVar(&targetPort, "target-port", 5000, "set the target host to return the access code") + loginCmd.Flags().BoolVarP(&forceLogin, "force", "f", false, "start the login process even with a valid token") + loginCmd.Flags().StringVar(&tokenPath, "token-path", ".ochami-token", "set the path the load/save the access token") + loginCmd.Flags().BoolVar(&noBrowser, "no-browser", false, "prevent the default browser from being opened automatically") + rootCmd.AddCommand(loginCmd) +} diff --git a/cmd/root.go b/cmd/root.go index 82d6b4e..ca4c25f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,6 +8,7 @@ import ( ) var ( + accessToken string timeout int threads int ports []int @@ -21,7 +22,7 @@ var ( drivers []string preferredDriver string ipmitoolPath string - outputPath string + outputPath string verbose bool ) diff --git a/internal/login.go b/internal/login.go new file mode 100644 index 0000000..508a987 --- /dev/null +++ b/internal/login.go @@ -0,0 +1,41 @@ +package magellan + +import ( + "fmt" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/pkg/browser" +) + +func Login(loginUrl string, targetHost string, targetPort int) (string, error) { + var accessToken string + + // check and make sure the login URL isn't empty + if loginUrl == "" { + return "", fmt.Errorf("no login URL provided") + } + + // if a target host and port are provided, then add to URL + if targetHost != "" && targetPort > 0 && targetPort < 65536 { + loginUrl += fmt.Sprintf("?target=http://%s:%d", targetHost, targetPort) + } + + // open browser with the specified URL + err := browser.OpenURL(loginUrl) + if err != nil { + return "", fmt.Errorf("failed to open browser: %v", err) + } + + // start a temporary server to listen for token + s := http.Server{ + Addr: fmt.Sprintf("%s:%d", targetHost, targetPort), + } + r := chi.NewRouter() + r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // try and extract access token from headers + accessToken = r.Header.Get("access_token") + s.Close() + }) + return accessToken, s.ListenAndServe() +}