diff --git a/cli/auth/authProvider.go b/cli/auth/authProvider.go index f56f3bbb0..ab7766dfc 100644 --- a/cli/auth/authProvider.go +++ b/cli/auth/authProvider.go @@ -4,12 +4,17 @@ import ( "context" "errors" "fmt" + "github.com/creasty/defaults" + "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" + "github.com/up9inc/mizu/cli/config" + "github.com/up9inc/mizu/cli/config/configStructs" "github.com/up9inc/mizu/cli/logger" "github.com/up9inc/mizu/cli/uiUtils" "golang.org/x/oauth2" "net" "net/http" + "os" "time" ) @@ -18,12 +23,60 @@ const loginTimeoutInMin = 2 // Ports are configured in keycloak "cli" client as valid redirect URIs. A change here must be reflected there as well. var listenPorts = []int{3141, 4001, 5002, 6003, 7004, 8005, 9006, 10007} -func LoginInteractively(envName string) (*oauth2.Token, error) { +func IsTokenExpired(tokenString string) (bool, error) { + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + return true, fmt.Errorf("failed to parse token, err: %v", err) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return true, fmt.Errorf("can't convert token's claims to standard claims") + } + + expiry := time.Unix(int64(claims["exp"].(float64)), 0) + + return time.Now().After(expiry), nil +} + +func Login() error { + token, err := loginInteractively() + if err != nil { + return fmt.Errorf("failed login interactively, err: %v", err) + } + + authConfig := configStructs.AuthConfig{ + EnvName: config.Config.Auth.EnvName, + Token: token.AccessToken, + } + + configFile := config.ConfigStruct{} + if err := defaults.Set(&configFile); err != nil { + return fmt.Errorf("failed inserting default values to config, err: %v", err) + } + + if err := config.LoadConfigFile(config.Config.ConfigFilePath, &configFile); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed getting config file, err: %v", err) + } + + configFile.Auth = authConfig + + if err := config.WriteConfig(&configFile); err != nil { + return fmt.Errorf("failed writing config with auth, err: %v", err) + } + + config.Config.Auth = authConfig + + logger.Log.Infof("Login successfully, token stored in config path: %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath)) + return nil +} + +func loginInteractively() (*oauth2.Token, error) { tokenChannel := make(chan *oauth2.Token) errorChannel := make(chan error) server := http.Server{} - go startLoginServer(tokenChannel, errorChannel, envName, &server) + go startLoginServer(tokenChannel, errorChannel, &server) defer func() { if err := server.Shutdown(context.Background()); err != nil { @@ -41,14 +94,14 @@ func LoginInteractively(envName string) (*oauth2.Token, error) { } } -func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, envName string, server *http.Server) { +func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, server *http.Server) { for _, port := range listenPorts { - var config = &oauth2.Config{ + var authConfig = &oauth2.Config{ ClientID: "cli", RedirectURL: fmt.Sprintf("http://localhost:%v/callback", port), Endpoint: oauth2.Endpoint{ - AuthURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/auth", envName), - TokenURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/token", envName), + AuthURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/auth", config.Config.Auth.EnvName), + TokenURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/token", config.Config.Auth.EnvName), }, } @@ -56,7 +109,7 @@ func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, mux := http.NewServeMux() server.Handler = mux - mux.Handle("/callback", loginCallbackHandler(tokenChannel, errorChannel, config, envName, state)) + mux.Handle("/callback", loginCallbackHandler(tokenChannel, errorChannel, authConfig, state)) listener, listenErr := net.Listen("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", port)) if listenErr != nil { @@ -64,7 +117,7 @@ func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, continue } - authorizationUrl := config.AuthCodeURL(state.String()) + authorizationUrl := authConfig.AuthCodeURL(state.String()) uiUtils.OpenBrowser(authorizationUrl) serveErr := server.Serve(listener) @@ -83,7 +136,7 @@ func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, errorChannel <- fmt.Errorf("failed to start serving on all listen ports, ports: %v", listenPorts) } -func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan error, config *oauth2.Config, envName string, state uuid.UUID) http.Handler { +func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan error, authConfig *oauth2.Config, state uuid.UUID) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if err := request.ParseForm(); err != nil { errorMsg := fmt.Sprintf("failed to parse form, err: %v", err) @@ -108,7 +161,7 @@ func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan err return } - token, err := config.Exchange(context.Background(), code) + token, err := authConfig.Exchange(context.Background(), code) if err != nil { errorMsg := fmt.Sprintf("failed to create token, err: %v", err) http.Error(writer, errorMsg, http.StatusInternalServerError) @@ -118,6 +171,6 @@ func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan err tokenChannel <- token - http.Redirect(writer, request, fmt.Sprintf("https://%s/CliLogin", envName), http.StatusFound) + http.Redirect(writer, request, fmt.Sprintf("https://%s/CliLogin", config.Config.Auth.EnvName), http.StatusFound) }) } diff --git a/cli/cmd/auth.go b/cli/cmd/auth.go deleted file mode 100644 index 97a8c7bcb..000000000 --- a/cli/cmd/auth.go +++ /dev/null @@ -1,15 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var authCmd = &cobra.Command{ - Use: "auth", - Short: "Authenticate to up9 application", -} - -func init() { - rootCmd.AddCommand(authCmd) -} - diff --git a/cli/cmd/authLogin.go b/cli/cmd/authLogin.go deleted file mode 100644 index fca09c402..000000000 --- a/cli/cmd/authLogin.go +++ /dev/null @@ -1,44 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/up9inc/mizu/cli/auth" - "github.com/up9inc/mizu/cli/config" - "github.com/up9inc/mizu/cli/config/configStructs" - "github.com/up9inc/mizu/cli/logger" - "github.com/up9inc/mizu/cli/telemetry" -) - -var authLoginCmd = &cobra.Command{ - Use: "login", - Short: "Login to up9 application", - RunE: func(cmd *cobra.Command, args []string) error { - go telemetry.ReportRun("authLogin", config.Config.Auth) - - token, err := auth.LoginInteractively(config.Config.Auth.EnvName) - if err != nil { - logger.Log.Errorf("Failed login interactively, err: %v", err) - return nil - } - - authConfig := configStructs.AuthConfig{ - EnvName: config.Config.Auth.EnvName, - Token: token.AccessToken, - } - - config.Config.Auth = authConfig - - if err := config.WriteConfig(&config.Config); err != nil { - logger.Log.Errorf("Failed writing config with auth, err: %v", err) - return nil - } - - logger.Log.Infof("Login successfully, token stored in config") - - return nil - }, -} - -func init() { - authCmd.AddCommand(authLoginCmd) -} diff --git a/cli/cmd/authLogout.go b/cli/cmd/authLogout.go deleted file mode 100644 index 96906f860..000000000 --- a/cli/cmd/authLogout.go +++ /dev/null @@ -1,31 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" - "github.com/up9inc/mizu/cli/config" - "github.com/up9inc/mizu/cli/logger" - "github.com/up9inc/mizu/cli/telemetry" -) - -var authLogoutCmd = &cobra.Command{ - Use: "logout", - Short: "Logout from up9 application", - RunE: func(cmd *cobra.Command, args []string) error { - go telemetry.ReportRun("authLogout", config.Config.Auth) - - config.Config.Auth.Token = "" - - if err := config.WriteConfig(&config.Config); err != nil { - logger.Log.Errorf("Failed writing config with default auth, err: %v", err) - return nil - } - - logger.Log.Infof("Logout successfully, token removed from config") - - return nil - }, -} - -func init() { - authCmd.AddCommand(authLogoutCmd) -} diff --git a/cli/config/config.go b/cli/config/config.go index 4a1580aee..788393929 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -39,7 +39,7 @@ func InitConfig(cmd *cobra.Command) error { configFilePathFlag := cmd.Flags().Lookup(ConfigFilePathCommandName) configFilePath := configFilePathFlag.Value.String() - if err := mergeConfigFile(configFilePath); err != nil { + if err := LoadConfigFile(configFilePath, &Config); err != nil { if configFilePathFlag.Changed || !os.IsNotExist(err) { return fmt.Errorf("invalid config, %w\n"+ "you can regenerate the file by removing it (%v) and using `mizu config -r`", err, configFilePath) @@ -80,7 +80,7 @@ func WriteConfig(config *ConfigStruct) error { return nil } -func mergeConfigFile(configFilePath string) error { +func LoadConfigFile(configFilePath string, config *ConfigStruct) error { reader, openErr := os.Open(configFilePath) if openErr != nil { return openErr @@ -91,10 +91,11 @@ func mergeConfigFile(configFilePath string) error { return readErr } - if err := yaml.Unmarshal(buf, &Config); err != nil { + if err := yaml.Unmarshal(buf, config); err != nil { return err } - logger.Log.Debugf("Found config file, merged to default options") + + logger.Log.Debugf("Found config file, config path: %s", configFilePath) return nil } diff --git a/cli/go.mod b/cli/go.mod index 11c0f116a..6e35a73c5 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/creasty/defaults v1.5.1 github.com/denisbrodbeck/machineid v1.0.1 + github.com/golang-jwt/jwt/v4 v4.1.0 github.com/google/go-github/v37 v37.0.0 github.com/google/uuid v1.1.2 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 diff --git a/cli/go.sum b/cli/go.sum index f274a030f..843dde79e 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -175,6 +175,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=