mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			d38e7b9cea
			...
			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.")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										141
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										141
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -2,8 +2,6 @@ package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	totpCmd "tinyauth/cmd/totp"
 | 
			
		||||
	userCmd "tinyauth/cmd/user"
 | 
			
		||||
	"tinyauth/internal/bootstrap"
 | 
			
		||||
	"tinyauth/internal/config"
 | 
			
		||||
	"tinyauth/internal/utils"
 | 
			
		||||
@@ -15,55 +13,28 @@ import (
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var rootCmd = &cobra.Command{
 | 
			
		||||
type rootCmd struct {
 | 
			
		||||
	root *cobra.Command
 | 
			
		||||
	cmd  *cobra.Command
 | 
			
		||||
 | 
			
		||||
	viper *viper.Viper
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newRootCmd() *rootCmd {
 | 
			
		||||
	return &rootCmd{
 | 
			
		||||
		viper: viper.New(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *rootCmd) Register() {
 | 
			
		||||
	c.cmd = &cobra.Command{
 | 
			
		||||
		Use:   "tinyauth",
 | 
			
		||||
	Short: "The simplest way to protect your apps with a login screen.",
 | 
			
		||||
	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)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal().Err(err).Msg("Failed to parse config")
 | 
			
		||||
		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,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 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() {
 | 
			
		||||
	rootCmd.FParseErrWhitelist.UnknownFlags = true
 | 
			
		||||
	err := rootCmd.Execute()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal().Err(err).Msg("Failed to execute command")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rootCmd.AddCommand(userCmd.UserCmd())
 | 
			
		||||
	rootCmd.AddCommand(totpCmd.TotpCmd())
 | 
			
		||||
 | 
			
		||||
	viper.AutomaticEnv()
 | 
			
		||||
	c.viper.AutomaticEnv()
 | 
			
		||||
 | 
			
		||||
	configOptions := []struct {
 | 
			
		||||
		name        string
 | 
			
		||||
@@ -101,17 +72,81 @@ func init() {
 | 
			
		||||
	for _, opt := range configOptions {
 | 
			
		||||
		switch v := opt.defaultVal.(type) {
 | 
			
		||||
		case bool:
 | 
			
		||||
			rootCmd.Flags().Bool(opt.name, v, opt.description)
 | 
			
		||||
			c.cmd.Flags().Bool(opt.name, v, opt.description)
 | 
			
		||||
		case int:
 | 
			
		||||
			rootCmd.Flags().Int(opt.name, v, opt.description)
 | 
			
		||||
			c.cmd.Flags().Int(opt.name, v, opt.description)
 | 
			
		||||
		case string:
 | 
			
		||||
			rootCmd.Flags().String(opt.name, v, opt.description)
 | 
			
		||||
			c.cmd.Flags().String(opt.name, v, opt.description)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Create uppercase env var 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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var versionCmd = &cobra.Command{
 | 
			
		||||
type versionCmd struct {
 | 
			
		||||
	root *cobra.Command
 | 
			
		||||
	cmd  *cobra.Command
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newVersionCmd(root *cobra.Command) *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: func(cmd *cobra.Command, args []string) {
 | 
			
		||||
		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)
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rootCmd.AddCommand(versionCmd)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user