mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			30fe695371
			...
			refactor/c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e1dcbda4ec | ||
| 
						 | 
					fa62ba2e33 | ||
| 
						 | 
					04b075c748 | ||
| 
						 | 
					74a360369d | 
							
								
								
									
										99
									
								
								cmd/create.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								cmd/create.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
				
			|||||||
 | 
					package cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/charmbracelet/huh"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type createUserCmd struct {
 | 
				
			||||||
 | 
						root *cobra.Command
 | 
				
			||||||
 | 
						cmd  *cobra.Command
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interactive bool
 | 
				
			||||||
 | 
						docker      bool
 | 
				
			||||||
 | 
						username    string
 | 
				
			||||||
 | 
						password    string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newCreateUserCmd(root *cobra.Command) *createUserCmd {
 | 
				
			||||||
 | 
						return &createUserCmd{
 | 
				
			||||||
 | 
							root: root,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *createUserCmd) Register() {
 | 
				
			||||||
 | 
						c.cmd = &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "create",
 | 
				
			||||||
 | 
							Short: "Create a user",
 | 
				
			||||||
 | 
							Long:  `Create a user either interactively or by passing flags.`,
 | 
				
			||||||
 | 
							Run:   c.run,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Create a user interactively")
 | 
				
			||||||
 | 
						c.cmd.Flags().BoolVar(&c.docker, "docker", false, "Format output for docker")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.root != nil {
 | 
				
			||||||
 | 
							c.root.AddCommand(c.cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *createUserCmd) GetCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return c.cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *createUserCmd) run(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
						log.Logger = log.Level(zerolog.InfoLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.interactive {
 | 
				
			||||||
 | 
							form := huh.NewForm(
 | 
				
			||||||
 | 
								huh.NewGroup(
 | 
				
			||||||
 | 
									huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("username cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("password cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									huh.NewSelect[bool]().Title("Format the output for Docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&c.docker),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							var baseTheme *huh.Theme = huh.ThemeBase()
 | 
				
			||||||
 | 
							err := form.WithTheme(baseTheme).Run()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatal().Err(err).Msg("Form failed")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.username == "" || c.password == "" {
 | 
				
			||||||
 | 
							log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Str("username", c.username).Msg("Creating user")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						passwd, err := bcrypt.GenerateFromPassword([]byte(c.password), bcrypt.DefaultCost)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to hash password")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If docker format is enabled, escape the dollar sign
 | 
				
			||||||
 | 
						passwdStr := string(passwd)
 | 
				
			||||||
 | 
						if c.docker {
 | 
				
			||||||
 | 
							passwdStr = strings.ReplaceAll(passwdStr, "$", "$$")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Str("user", fmt.Sprintf("%s:%s", c.username, passwdStr)).Msg("User created")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										120
									
								
								cmd/generate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								cmd/generate.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					package cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"tinyauth/internal/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/charmbracelet/huh"
 | 
				
			||||||
 | 
						"github.com/mdp/qrterminal/v3"
 | 
				
			||||||
 | 
						"github.com/pquerna/otp/totp"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type generateTotpCmd struct {
 | 
				
			||||||
 | 
						root *cobra.Command
 | 
				
			||||||
 | 
						cmd  *cobra.Command
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interactive bool
 | 
				
			||||||
 | 
						user        string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newGenerateTotpCmd(root *cobra.Command) *generateTotpCmd {
 | 
				
			||||||
 | 
						return &generateTotpCmd{
 | 
				
			||||||
 | 
							root: root,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *generateTotpCmd) Register() {
 | 
				
			||||||
 | 
						c.cmd = &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "generate",
 | 
				
			||||||
 | 
							Short: "Generate a totp secret",
 | 
				
			||||||
 | 
							Long:  `Generate a totp secret for a user either interactively or by passing flags.`,
 | 
				
			||||||
 | 
							Run:   c.run,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Run in interactive mode")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.user, "user", "", "Your current user (username:hash)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.root != nil {
 | 
				
			||||||
 | 
							c.root.AddCommand(c.cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *generateTotpCmd) GetCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return c.cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *generateTotpCmd) run(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
						log.Logger = log.Level(zerolog.InfoLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.interactive {
 | 
				
			||||||
 | 
							form := huh.NewForm(
 | 
				
			||||||
 | 
								huh.NewGroup(
 | 
				
			||||||
 | 
									huh.NewInput().Title("Current user (username:hash)").Value(&c.user).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("user cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							var baseTheme *huh.Theme = huh.ThemeBase()
 | 
				
			||||||
 | 
							err := form.WithTheme(baseTheme).Run()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatal().Err(err).Msg("Form failed")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := utils.ParseUser(c.user)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to parse user")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						docker := false
 | 
				
			||||||
 | 
						if strings.Contains(c.user, "$$") {
 | 
				
			||||||
 | 
							docker = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user.TotpSecret != "" {
 | 
				
			||||||
 | 
							log.Fatal().Msg("User already has a TOTP secret")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						key, err := totp.Generate(totp.GenerateOpts{
 | 
				
			||||||
 | 
							Issuer:      "Tinyauth",
 | 
				
			||||||
 | 
							AccountName: user.Username,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to generate TOTP secret")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secret := key.Secret()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Str("secret", secret).Msg("Generated TOTP secret")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Msg("Generated QR code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						config := qrterminal.Config{
 | 
				
			||||||
 | 
							Level:     qrterminal.L,
 | 
				
			||||||
 | 
							Writer:    os.Stdout,
 | 
				
			||||||
 | 
							BlackChar: qrterminal.BLACK,
 | 
				
			||||||
 | 
							WhiteChar: qrterminal.WHITE,
 | 
				
			||||||
 | 
							QuietZone: 2,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qrterminal.GenerateWithConfig(key.URL(), config)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user.TotpSecret = secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If using docker escape re-escape it
 | 
				
			||||||
 | 
						if docker {
 | 
				
			||||||
 | 
							user.Password = strings.ReplaceAll(user.Password, "$", "$$")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										133
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -2,8 +2,6 @@ package cmd
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	totpCmd "tinyauth/cmd/totp"
 | 
					 | 
				
			||||||
	userCmd "tinyauth/cmd/user"
 | 
					 | 
				
			||||||
	"tinyauth/internal/bootstrap"
 | 
						"tinyauth/internal/bootstrap"
 | 
				
			||||||
	"tinyauth/internal/config"
 | 
						"tinyauth/internal/config"
 | 
				
			||||||
	"tinyauth/internal/utils"
 | 
						"tinyauth/internal/utils"
 | 
				
			||||||
@@ -15,55 +13,28 @@ import (
 | 
				
			|||||||
	"github.com/spf13/viper"
 | 
						"github.com/spf13/viper"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var rootCmd = &cobra.Command{
 | 
					type rootCmd struct {
 | 
				
			||||||
	Use:   "tinyauth",
 | 
						root *cobra.Command
 | 
				
			||||||
	Short: "The simplest way to protect your apps with a login screen.",
 | 
						cmd  *cobra.Command
 | 
				
			||||||
	Long:  `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
 | 
					 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
					 | 
				
			||||||
		var conf config.Config
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		err := viper.Unmarshal(&conf)
 | 
						viper *viper.Viper
 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to parse config")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Validate config
 | 
					 | 
				
			||||||
		v := validator.New()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = v.Struct(conf)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Invalid config")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
 | 
					 | 
				
			||||||
		log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting tinyauth")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create bootstrap app
 | 
					 | 
				
			||||||
		app := bootstrap.NewBootstrapApp(conf)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Run
 | 
					 | 
				
			||||||
		err = app.Setup()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to setup app")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func Execute() {
 | 
					func newRootCmd() *rootCmd {
 | 
				
			||||||
	rootCmd.FParseErrWhitelist.UnknownFlags = true
 | 
						return &rootCmd{
 | 
				
			||||||
	err := rootCmd.Execute()
 | 
							viper: viper.New(),
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		log.Fatal().Err(err).Msg("Failed to execute command")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func (c *rootCmd) Register() {
 | 
				
			||||||
	rootCmd.AddCommand(userCmd.UserCmd())
 | 
						c.cmd = &cobra.Command{
 | 
				
			||||||
	rootCmd.AddCommand(totpCmd.TotpCmd())
 | 
							Use:   "tinyauth",
 | 
				
			||||||
 | 
							Short: "The simplest way to protect your apps with a login screen",
 | 
				
			||||||
 | 
							Long:  `Tinyauth is a simple authentication middleware that adds a simple login screen or OAuth with Google, Github or any other provider to all of your docker apps.`,
 | 
				
			||||||
 | 
							Run:   c.run,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	viper.AutomaticEnv()
 | 
						c.viper.AutomaticEnv()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	configOptions := []struct {
 | 
						configOptions := []struct {
 | 
				
			||||||
		name        string
 | 
							name        string
 | 
				
			||||||
@@ -101,17 +72,81 @@ func init() {
 | 
				
			|||||||
	for _, opt := range configOptions {
 | 
						for _, opt := range configOptions {
 | 
				
			||||||
		switch v := opt.defaultVal.(type) {
 | 
							switch v := opt.defaultVal.(type) {
 | 
				
			||||||
		case bool:
 | 
							case bool:
 | 
				
			||||||
			rootCmd.Flags().Bool(opt.name, v, opt.description)
 | 
								c.cmd.Flags().Bool(opt.name, v, opt.description)
 | 
				
			||||||
		case int:
 | 
							case int:
 | 
				
			||||||
			rootCmd.Flags().Int(opt.name, v, opt.description)
 | 
								c.cmd.Flags().Int(opt.name, v, opt.description)
 | 
				
			||||||
		case string:
 | 
							case string:
 | 
				
			||||||
			rootCmd.Flags().String(opt.name, v, opt.description)
 | 
								c.cmd.Flags().String(opt.name, v, opt.description)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create uppercase env var name
 | 
							// Create uppercase env var name
 | 
				
			||||||
		envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_")
 | 
							envVar := strings.ReplaceAll(strings.ToUpper(opt.name), "-", "_")
 | 
				
			||||||
		viper.BindEnv(opt.name, envVar)
 | 
							c.viper.BindEnv(opt.name, envVar)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	viper.BindPFlags(rootCmd.Flags())
 | 
						c.viper.BindPFlags(c.cmd.Flags())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.root != nil {
 | 
				
			||||||
 | 
							c.root.AddCommand(c.cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *rootCmd) GetCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return c.cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *rootCmd) run(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
						var conf config.Config
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := c.viper.Unmarshal(&conf)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to parse config")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						v := validator.New()
 | 
				
			||||||
 | 
						err = v.Struct(conf)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Invalid config")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Logger = log.Level(zerolog.Level(utils.GetLogLevel(conf.LogLevel)))
 | 
				
			||||||
 | 
						log.Info().Str("version", strings.TrimSpace(config.Version)).Msg("Starting Tinyauth")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						app := bootstrap.NewBootstrapApp(conf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = app.Setup()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to setup app")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Run() {
 | 
				
			||||||
 | 
						rootCmd := newRootCmd()
 | 
				
			||||||
 | 
						rootCmd.Register()
 | 
				
			||||||
 | 
						root := rootCmd.GetCmd()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						userCmd := &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "user",
 | 
				
			||||||
 | 
							Short: "User utilities",
 | 
				
			||||||
 | 
							Long:  `Utilities for creating and verifying tinyauth compatible users.`,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						totpCmd := &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "totp",
 | 
				
			||||||
 | 
							Short: "Totp utilities",
 | 
				
			||||||
 | 
							Long:  `Utilities for creating and verifying totp codes.`,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newCreateUserCmd(userCmd).Register()
 | 
				
			||||||
 | 
						newVerifyUserCmd(userCmd).Register()
 | 
				
			||||||
 | 
						newGenerateTotpCmd(totpCmd).Register()
 | 
				
			||||||
 | 
						newVersionCmd(root).Register()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						root.AddCommand(userCmd)
 | 
				
			||||||
 | 
						root.AddCommand(totpCmd)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := root.Execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to execute root command")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,99 +0,0 @@
 | 
				
			|||||||
package generate
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"os"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
	"tinyauth/internal/utils"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/charmbracelet/huh"
 | 
					 | 
				
			||||||
	"github.com/mdp/qrterminal/v3"
 | 
					 | 
				
			||||||
	"github.com/pquerna/otp/totp"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
					 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var interactive bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Input user
 | 
					 | 
				
			||||||
var iUser string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var GenerateCmd = &cobra.Command{
 | 
					 | 
				
			||||||
	Use:   "generate",
 | 
					 | 
				
			||||||
	Short: "Generate a totp secret",
 | 
					 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
					 | 
				
			||||||
		log.Logger = log.Level(zerolog.InfoLevel)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if interactive {
 | 
					 | 
				
			||||||
			form := huh.NewForm(
 | 
					 | 
				
			||||||
				huh.NewGroup(
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("user cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			var baseTheme *huh.Theme = huh.ThemeBase()
 | 
					 | 
				
			||||||
			err := form.WithTheme(baseTheme).Run()
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Fatal().Err(err).Msg("Form failed")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user, err := utils.ParseUser(iUser)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to parse user")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		dockerEscape := false
 | 
					 | 
				
			||||||
		if strings.Contains(iUser, "$$") {
 | 
					 | 
				
			||||||
			dockerEscape = true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if user.TotpSecret != "" {
 | 
					 | 
				
			||||||
			log.Fatal().Msg("User already has a totp secret")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		key, err := totp.Generate(totp.GenerateOpts{
 | 
					 | 
				
			||||||
			Issuer:      "Tinyauth",
 | 
					 | 
				
			||||||
			AccountName: user.Username,
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to generate totp secret")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		secret := key.Secret()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Str("secret", secret).Msg("Generated totp secret")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Msg("Generated QR code")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		config := qrterminal.Config{
 | 
					 | 
				
			||||||
			Level:     qrterminal.L,
 | 
					 | 
				
			||||||
			Writer:    os.Stdout,
 | 
					 | 
				
			||||||
			BlackChar: qrterminal.BLACK,
 | 
					 | 
				
			||||||
			WhiteChar: qrterminal.WHITE,
 | 
					 | 
				
			||||||
			QuietZone: 2,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		qrterminal.GenerateWithConfig(key.URL(), config)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user.TotpSecret = secret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// If using docker escape re-escape it
 | 
					 | 
				
			||||||
		if dockerEscape {
 | 
					 | 
				
			||||||
			user.Password = strings.ReplaceAll(user.Password, "$", "$$")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Add the totp secret to your authenticator app then use the verify command to ensure everything is working correctly.")
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode")
 | 
					 | 
				
			||||||
	GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,17 +0,0 @@
 | 
				
			|||||||
package cmd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"tinyauth/cmd/totp/generate"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func TotpCmd() *cobra.Command {
 | 
					 | 
				
			||||||
	totpCmd := &cobra.Command{
 | 
					 | 
				
			||||||
		Use:   "totp",
 | 
					 | 
				
			||||||
		Short: "Totp utilities",
 | 
					 | 
				
			||||||
		Long:  `Utilities for creating and verifying totp codes.`,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	totpCmd.AddCommand(generate.GenerateCmd)
 | 
					 | 
				
			||||||
	return totpCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,80 +0,0 @@
 | 
				
			|||||||
package create
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"strings"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/charmbracelet/huh"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
					 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
					 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var interactive bool
 | 
					 | 
				
			||||||
var docker bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// i stands for input
 | 
					 | 
				
			||||||
var iUsername string
 | 
					 | 
				
			||||||
var iPassword string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var CreateCmd = &cobra.Command{
 | 
					 | 
				
			||||||
	Use:   "create",
 | 
					 | 
				
			||||||
	Short: "Create a user",
 | 
					 | 
				
			||||||
	Long:  `Create a user either interactively or by passing flags.`,
 | 
					 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
					 | 
				
			||||||
		log.Logger = log.Level(zerolog.InfoLevel)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if interactive {
 | 
					 | 
				
			||||||
			form := huh.NewForm(
 | 
					 | 
				
			||||||
				huh.NewGroup(
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("username cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("password cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
					huh.NewSelect[bool]().Title("Format the output for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			var baseTheme *huh.Theme = huh.ThemeBase()
 | 
					 | 
				
			||||||
			err := form.WithTheme(baseTheme).Run()
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Fatal().Err(err).Msg("Form failed")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if iUsername == "" || iPassword == "" {
 | 
					 | 
				
			||||||
			log.Fatal().Err(errors.New("error invalid input")).Msg("Username and password cannot be empty")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		password, err := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to hash password")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// If docker format is enabled, escape the dollar sign
 | 
					 | 
				
			||||||
		passwordString := string(password)
 | 
					 | 
				
			||||||
		if docker {
 | 
					 | 
				
			||||||
			passwordString = strings.ReplaceAll(passwordString, "$", "$$")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created")
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
 | 
					 | 
				
			||||||
	CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
 | 
					 | 
				
			||||||
	CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username")
 | 
					 | 
				
			||||||
	CreateCmd.Flags().StringVar(&iPassword, "password", "", "Password")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
package cmd
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"tinyauth/cmd/user/create"
 | 
					 | 
				
			||||||
	"tinyauth/cmd/user/verify"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func UserCmd() *cobra.Command {
 | 
					 | 
				
			||||||
	userCmd := &cobra.Command{
 | 
					 | 
				
			||||||
		Use:   "user",
 | 
					 | 
				
			||||||
		Short: "User utilities",
 | 
					 | 
				
			||||||
		Long:  `Utilities for creating and verifying tinyauth compatible users.`,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	userCmd.AddCommand(create.CreateCmd)
 | 
					 | 
				
			||||||
	userCmd.AddCommand(verify.VerifyCmd)
 | 
					 | 
				
			||||||
	return userCmd
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,101 +0,0 @@
 | 
				
			|||||||
package verify
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import (
 | 
					 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"tinyauth/internal/utils"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	"github.com/charmbracelet/huh"
 | 
					 | 
				
			||||||
	"github.com/pquerna/otp/totp"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog"
 | 
					 | 
				
			||||||
	"github.com/rs/zerolog/log"
 | 
					 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
					 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var interactive bool
 | 
					 | 
				
			||||||
var docker bool
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// i stands for input
 | 
					 | 
				
			||||||
var iUsername string
 | 
					 | 
				
			||||||
var iPassword string
 | 
					 | 
				
			||||||
var iTotp string
 | 
					 | 
				
			||||||
var iUser string
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var VerifyCmd = &cobra.Command{
 | 
					 | 
				
			||||||
	Use:   "verify",
 | 
					 | 
				
			||||||
	Short: "Verify a user is set up correctly",
 | 
					 | 
				
			||||||
	Long:  `Verify a user is set up correctly meaning that it has a correct username, password and totp code.`,
 | 
					 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
					 | 
				
			||||||
		log.Logger = log.Level(zerolog.InfoLevel)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if interactive {
 | 
					 | 
				
			||||||
			form := huh.NewForm(
 | 
					 | 
				
			||||||
				huh.NewGroup(
 | 
					 | 
				
			||||||
					huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("user cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("username cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
 | 
					 | 
				
			||||||
						if s == "" {
 | 
					 | 
				
			||||||
							return errors.New("password cannot be empty")
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						return nil
 | 
					 | 
				
			||||||
					})),
 | 
					 | 
				
			||||||
					huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp),
 | 
					 | 
				
			||||||
				),
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
			var baseTheme *huh.Theme = huh.ThemeBase()
 | 
					 | 
				
			||||||
			err := form.WithTheme(baseTheme).Run()
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				log.Fatal().Err(err).Msg("Form failed")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		user, err := utils.ParseUser(iUser)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Err(err).Msg("Failed to parse user")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if user.Username != iUsername {
 | 
					 | 
				
			||||||
			log.Fatal().Msg("Username is incorrect")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			log.Fatal().Msg("Password is incorrect")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if user.TotpSecret == "" {
 | 
					 | 
				
			||||||
			if iTotp != "" {
 | 
					 | 
				
			||||||
				log.Warn().Msg("User does not have 2fa secret")
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			log.Info().Msg("User verified")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ok := totp.Validate(iTotp, user.TotpSecret)
 | 
					 | 
				
			||||||
		if !ok {
 | 
					 | 
				
			||||||
			log.Fatal().Msg("Totp code incorrect")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		log.Info().Msg("User verified")
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
func init() {
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password")
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code")
 | 
					 | 
				
			||||||
	VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)")
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										118
									
								
								cmd/verify.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								cmd/verify.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					package cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"tinyauth/internal/utils"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/charmbracelet/huh"
 | 
				
			||||||
 | 
						"github.com/pquerna/otp/totp"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog"
 | 
				
			||||||
 | 
						"github.com/rs/zerolog/log"
 | 
				
			||||||
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type verifyUserCmd struct {
 | 
				
			||||||
 | 
						root *cobra.Command
 | 
				
			||||||
 | 
						cmd  *cobra.Command
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						interactive bool
 | 
				
			||||||
 | 
						username    string
 | 
				
			||||||
 | 
						password    string
 | 
				
			||||||
 | 
						totp        string
 | 
				
			||||||
 | 
						user        string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func newVerifyUserCmd(root *cobra.Command) *verifyUserCmd {
 | 
				
			||||||
 | 
						return &verifyUserCmd{
 | 
				
			||||||
 | 
							root: root,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *verifyUserCmd) Register() {
 | 
				
			||||||
 | 
						c.cmd = &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "verify",
 | 
				
			||||||
 | 
							Short: "Verify a user is set up correctly",
 | 
				
			||||||
 | 
							Long:  `Verify a user is set up correctly meaning that it has a correct username, password and TOTP code.`,
 | 
				
			||||||
 | 
							Run:   c.run,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.cmd.Flags().BoolVarP(&c.interactive, "interactive", "i", false, "Validate a user interactively")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.username, "username", "", "Username")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.password, "password", "", "Password")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.totp, "totp", "", "TOTP code")
 | 
				
			||||||
 | 
						c.cmd.Flags().StringVar(&c.user, "user", "", "Hash (username:hash:totp)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.root != nil {
 | 
				
			||||||
 | 
							c.root.AddCommand(c.cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *verifyUserCmd) GetCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return c.cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *verifyUserCmd) run(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
						log.Logger = log.Level(zerolog.InfoLevel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.interactive {
 | 
				
			||||||
 | 
							form := huh.NewForm(
 | 
				
			||||||
 | 
								huh.NewGroup(
 | 
				
			||||||
 | 
									huh.NewInput().Title("User (username:hash:totp)").Value(&c.user).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("user cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									huh.NewInput().Title("Username").Value(&c.username).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("username cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									huh.NewInput().Title("Password").Value(&c.password).Validate((func(s string) error {
 | 
				
			||||||
 | 
										if s == "" {
 | 
				
			||||||
 | 
											return errors.New("password cannot be empty")
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										return nil
 | 
				
			||||||
 | 
									})),
 | 
				
			||||||
 | 
									huh.NewInput().Title("TOTP Code (optional)").Value(&c.totp),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							var baseTheme *huh.Theme = huh.ThemeBase()
 | 
				
			||||||
 | 
							err := form.WithTheme(baseTheme).Run()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatal().Err(err).Msg("Form failed")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user, err := utils.ParseUser(c.user)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Err(err).Msg("Failed to parse user")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user.Username != c.username {
 | 
				
			||||||
 | 
							log.Fatal().Msg("Username is incorrect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(c.password))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatal().Msg("Password is incorrect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if user.TotpSecret == "" {
 | 
				
			||||||
 | 
							if c.totp != "" {
 | 
				
			||||||
 | 
								log.Warn().Msg("User does not have TOTP secret")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Info().Msg("User verified")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ok := totp.Validate(c.totp, user.TotpSecret)
 | 
				
			||||||
 | 
						if !ok {
 | 
				
			||||||
 | 
							log.Fatal().Msg("TOTP code incorrect")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						log.Info().Msg("User verified")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,17 +7,36 @@ import (
 | 
				
			|||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var versionCmd = &cobra.Command{
 | 
					type versionCmd struct {
 | 
				
			||||||
	Use:   "version",
 | 
						root *cobra.Command
 | 
				
			||||||
	Short: "Print the version number of Tinyauth",
 | 
						cmd  *cobra.Command
 | 
				
			||||||
	Long:  `All software has versions. This is Tinyauth's`,
 | 
					 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
					 | 
				
			||||||
		fmt.Printf("Version: %s\n", config.Version)
 | 
					 | 
				
			||||||
		fmt.Printf("Commit Hash: %s\n", config.CommitHash)
 | 
					 | 
				
			||||||
		fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func newVersionCmd(root *cobra.Command) *versionCmd {
 | 
				
			||||||
	rootCmd.AddCommand(versionCmd)
 | 
						return &versionCmd{
 | 
				
			||||||
 | 
							root: root,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *versionCmd) Register() {
 | 
				
			||||||
 | 
						c.cmd = &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "version",
 | 
				
			||||||
 | 
							Short: "Print the version number of Tinyauth",
 | 
				
			||||||
 | 
							Long:  `All software has versions. This is Tinyauth's.`,
 | 
				
			||||||
 | 
							Run:   c.run,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if c.root != nil {
 | 
				
			||||||
 | 
							c.root.AddCommand(c.cmd)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *versionCmd) GetCmd() *cobra.Command {
 | 
				
			||||||
 | 
						return c.cmd
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c *versionCmd) run(cmd *cobra.Command, args []string) {
 | 
				
			||||||
 | 
						fmt.Printf("Version: %s\n", config.Version)
 | 
				
			||||||
 | 
						fmt.Printf("Commit Hash: %s\n", config.CommitHash)
 | 
				
			||||||
 | 
						fmt.Printf("Build Timestamp: %s\n", config.BuildTimestamp)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							@@ -11,5 +11,5 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func main() {
 | 
					func main() {
 | 
				
			||||||
	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger()
 | 
						log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Caller().Logger()
 | 
				
			||||||
	cmd.Execute()
 | 
						cmd.Run()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user