mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 20:55:42 +00:00
feat: finalize totp gen code
This commit is contained in:
@@ -15,6 +15,12 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Interactive flag
|
||||||
|
var interactive bool
|
||||||
|
|
||||||
|
// i stands for input
|
||||||
|
var iUser string
|
||||||
|
|
||||||
var GenerateCmd = &cobra.Command{
|
var GenerateCmd = &cobra.Command{
|
||||||
Use: "generate",
|
Use: "generate",
|
||||||
Short: "Generate a totp secret",
|
Short: "Generate a totp secret",
|
||||||
@@ -22,43 +28,45 @@ var GenerateCmd = &cobra.Command{
|
|||||||
// Setup logger
|
// Setup logger
|
||||||
log.Logger = log.Level(zerolog.InfoLevel)
|
log.Logger = log.Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
// Variables
|
|
||||||
var userStr string
|
|
||||||
var totpCode string
|
|
||||||
|
|
||||||
// Use simple theme
|
// Use simple theme
|
||||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||||
|
|
||||||
// Create huh form
|
// Interactive
|
||||||
form := huh.NewForm(
|
if interactive {
|
||||||
huh.NewGroup(
|
// Create huh form
|
||||||
huh.NewInput().Title("User (username:hash)").Value(&userStr).Validate((func(s string) error {
|
form := huh.NewForm(
|
||||||
if s == "" {
|
huh.NewGroup(
|
||||||
return errors.New("user cannot be empty")
|
huh.NewInput().Title("Current username:hash").Value(&iUser).Validate((func(s string) error {
|
||||||
}
|
if s == "" {
|
||||||
return nil
|
return errors.New("user cannot be empty")
|
||||||
})),
|
}
|
||||||
),
|
return nil
|
||||||
)
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
formErr := form.WithTheme(baseTheme).Run()
|
// Run form
|
||||||
|
formErr := form.WithTheme(baseTheme).Run()
|
||||||
|
|
||||||
if formErr != nil {
|
if formErr != nil {
|
||||||
log.Fatal().Err(formErr).Msg("Form failed")
|
log.Fatal().Err(formErr).Msg("Form failed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove double dollar signs
|
|
||||||
userStr = strings.ReplaceAll(userStr, "$$", "$")
|
|
||||||
|
|
||||||
log.Info().Str("user", userStr).Msg("User")
|
|
||||||
|
|
||||||
// Parse user
|
// Parse user
|
||||||
user, parseErr := utils.ParseUser(userStr)
|
user, parseErr := utils.ParseUser(iUser)
|
||||||
|
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
log.Fatal().Err(parseErr).Msg("Failed to parse user")
|
log.Fatal().Err(parseErr).Msg("Failed to parse user")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user was using docker escape
|
||||||
|
dockerEscape := false
|
||||||
|
|
||||||
|
if strings.Contains(user.Username, "$$") {
|
||||||
|
dockerEscape = true
|
||||||
|
}
|
||||||
|
|
||||||
// Check it has totp
|
// Check it has totp
|
||||||
if user.TotpSecret != "" {
|
if user.TotpSecret != "" {
|
||||||
log.Fatal().Msg("User already has a totp secret")
|
log.Fatal().Msg("User already has a totp secret")
|
||||||
@@ -93,48 +101,21 @@ var GenerateCmd = &cobra.Command{
|
|||||||
|
|
||||||
qrterminal.GenerateWithConfig(key.URL(), config)
|
qrterminal.GenerateWithConfig(key.URL(), config)
|
||||||
|
|
||||||
// Wait for verify
|
// Add the secret to the user
|
||||||
log.Info().Msg("Scan the QR code with your authenticator app then press enter to verify")
|
|
||||||
|
|
||||||
// Wait for enter
|
|
||||||
var input string
|
|
||||||
_, _ = fmt.Scanln(&input)
|
|
||||||
|
|
||||||
// Move cursor up and overwrite the line
|
|
||||||
fmt.Print("\033[F\033[K")
|
|
||||||
|
|
||||||
// Create huh form
|
|
||||||
form = huh.NewForm(
|
|
||||||
huh.NewGroup(
|
|
||||||
huh.NewInput().Title("Code").Value(&totpCode).Validate((func(s string) error {
|
|
||||||
if s == "" {
|
|
||||||
return errors.New("code cannot be empty")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
formErr = form.WithTheme(baseTheme).Run()
|
|
||||||
|
|
||||||
if formErr != nil {
|
|
||||||
log.Fatal().Err(formErr).Msg("Form failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify code
|
|
||||||
codeOk := totp.Validate(totpCode, secret)
|
|
||||||
|
|
||||||
if !codeOk {
|
|
||||||
log.Fatal().Msg("Failed to verify code")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user
|
|
||||||
user.TotpSecret = secret
|
user.TotpSecret = secret
|
||||||
|
|
||||||
|
// If using docker escape re-escape it
|
||||||
|
if dockerEscape {
|
||||||
|
user.Password = strings.ReplaceAll(user.Password, "$", "$$")
|
||||||
|
}
|
||||||
|
|
||||||
// Print success
|
// Print success
|
||||||
log.Info().Str("user", fmt.Sprintf("%s:%s:%s", user.Username, user.Password, user.TotpSecret)).Msg("Code verified, get your new user")
|
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() {
|
func init() {
|
||||||
|
// Add interactive flag
|
||||||
|
GenerateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Run in interactive mode")
|
||||||
|
GenerateCmd.Flags().StringVar(&iUser, "user", "", "Your current username:hash")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"tinyauth/internal/utils"
|
||||||
|
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -17,22 +18,26 @@ var docker bool
|
|||||||
// i stands for input
|
// i stands for input
|
||||||
var iUsername string
|
var iUsername string
|
||||||
var iPassword string
|
var iPassword string
|
||||||
|
var iTotp string
|
||||||
var iUser string
|
var iUser string
|
||||||
|
|
||||||
var VerifyCmd = &cobra.Command{
|
var VerifyCmd = &cobra.Command{
|
||||||
Use: "verify",
|
Use: "verify",
|
||||||
Short: "Verify a user is set up correctly",
|
Short: "Verify a user is set up correctly",
|
||||||
Long: `Verify a user is set up correctly meaning that it has a correct username and password.`,
|
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) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
// Setup logger
|
// Setup logger
|
||||||
log.Logger = log.Level(zerolog.InfoLevel)
|
log.Logger = log.Level(zerolog.InfoLevel)
|
||||||
|
|
||||||
|
// Use simple theme
|
||||||
|
var baseTheme *huh.Theme = huh.ThemeBase()
|
||||||
|
|
||||||
// Check if interactive
|
// Check if interactive
|
||||||
if interactive {
|
if interactive {
|
||||||
// Create huh form
|
// Create huh form
|
||||||
form := huh.NewForm(
|
form := huh.NewForm(
|
||||||
huh.NewGroup(
|
huh.NewGroup(
|
||||||
huh.NewInput().Title("User (username:hash)").Value(&iUser).Validate((func(s string) error {
|
huh.NewInput().Title("User (username:hash:totp)").Value(&iUser).Validate((func(s string) error {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return errors.New("user cannot be empty")
|
return errors.New("user cannot be empty")
|
||||||
}
|
}
|
||||||
@@ -50,13 +55,11 @@ var VerifyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})),
|
})),
|
||||||
huh.NewSelect[bool]().Title("Is the user formatted for docker?").Options(huh.NewOption("Yes", true), huh.NewOption("No", false)).Value(&docker),
|
huh.NewInput().Title("Totp Code (if setup)").Value(&iTotp),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use simple theme
|
// Run form
|
||||||
var baseTheme *huh.Theme = huh.ThemeBase()
|
|
||||||
|
|
||||||
formErr := form.WithTheme(baseTheme).Run()
|
formErr := form.WithTheme(baseTheme).Run()
|
||||||
|
|
||||||
if formErr != nil {
|
if formErr != nil {
|
||||||
@@ -64,33 +67,44 @@ var VerifyCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we have username, password and user?
|
// Parse user
|
||||||
if iUsername == "" || iPassword == "" || iUser == "" {
|
user, userErr := utils.ParseUser(iUser)
|
||||||
log.Fatal().Msg("Username, password and user cannot be empty")
|
|
||||||
|
if userErr != nil {
|
||||||
|
log.Fatal().Err(userErr).Msg("Failed to parse user")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info().Str("user", iUser).Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Verifying user")
|
// Compare username
|
||||||
|
if user.Username != iUsername {
|
||||||
// Split username and password hash
|
log.Fatal().Msg("Username is incorrect")
|
||||||
username, hash, ok := strings.Cut(iUser, ":")
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
log.Fatal().Msg("User is not formatted correctly")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace $$ with $ if formatted for docker
|
// Compare password
|
||||||
if docker {
|
verifyErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(iPassword))
|
||||||
hash = strings.ReplaceAll(hash, "$$", "$")
|
|
||||||
|
if verifyErr != nil {
|
||||||
|
log.Fatal().Msg("Ppassword is incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare username and password
|
// Check if user has 2fa code
|
||||||
verifyErr := bcrypt.CompareHashAndPassword([]byte(hash), []byte(iPassword))
|
if user.TotpSecret == "" {
|
||||||
|
if iTotp != "" {
|
||||||
if verifyErr != nil || username != iUsername {
|
log.Warn().Msg("User does not have 2fa secret")
|
||||||
log.Fatal().Msg("Username or password incorrect")
|
}
|
||||||
} else {
|
log.Info().Msg("User verified")
|
||||||
log.Info().Msg("Verification successful")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check totp code
|
||||||
|
totpOk := totp.Validate(iTotp, user.TotpSecret)
|
||||||
|
|
||||||
|
if !totpOk {
|
||||||
|
log.Fatal().Msg("Totp code incorrect")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done
|
||||||
|
log.Info().Msg("User verified")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,5 +114,6 @@ func init() {
|
|||||||
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
|
||||||
VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
|
VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
|
||||||
VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password")
|
VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password")
|
||||||
VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash combination)")
|
VerifyCmd.Flags().StringVar(&iTotp, "totp", "", "Totp code")
|
||||||
|
VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash:totp combination)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,6 +218,11 @@ func Filter[T any](slice []T, test func(T) bool) (res []T) {
|
|||||||
|
|
||||||
// Parse user
|
// Parse user
|
||||||
func ParseUser(user string) (types.User, error) {
|
func ParseUser(user string) (types.User, error) {
|
||||||
|
// Check if the user is escaped
|
||||||
|
if strings.Contains(user, "$$") {
|
||||||
|
user = strings.ReplaceAll(user, "$$", "$")
|
||||||
|
}
|
||||||
|
|
||||||
// Split the user by colon
|
// Split the user by colon
|
||||||
userSplit := strings.Split(user, ":")
|
userSplit := strings.Split(user, ":")
|
||||||
|
|
||||||
@@ -228,12 +233,21 @@ func ParseUser(user string) (types.User, error) {
|
|||||||
|
|
||||||
// Check if the user has a totp secret
|
// Check if the user has a totp secret
|
||||||
if len(userSplit) == 2 {
|
if len(userSplit) == 2 {
|
||||||
|
// Check for empty username or password
|
||||||
|
if userSplit[1] == "" || userSplit[0] == "" {
|
||||||
|
return types.User{}, errors.New("invalid user format")
|
||||||
|
}
|
||||||
return types.User{
|
return types.User{
|
||||||
Username: userSplit[0],
|
Username: userSplit[0],
|
||||||
Password: userSplit[1],
|
Password: userSplit[1],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for empty username, password or totp secret
|
||||||
|
if userSplit[2] == "" || userSplit[1] == "" || userSplit[0] == "" {
|
||||||
|
return types.User{}, errors.New("invalid user format")
|
||||||
|
}
|
||||||
|
|
||||||
// Return the user struct
|
// Return the user struct
|
||||||
return types.User{
|
return types.User{
|
||||||
Username: userSplit[0],
|
Username: userSplit[0],
|
||||||
|
|||||||
@@ -36,18 +36,6 @@ func TestParseUsers(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(expected, result) {
|
if !reflect.DeepEqual(expected, result) {
|
||||||
t.Fatalf("Expected %v, got %v", expected, result)
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Log("Testing parse users with an invalid string")
|
|
||||||
|
|
||||||
// Test the parse users function with an invalid string
|
|
||||||
users = "user1:pass1,user2"
|
|
||||||
|
|
||||||
_, err = utils.ParseUsers(users)
|
|
||||||
|
|
||||||
// There should be an error
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error parsing users")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test the get root url function
|
// Test the get root url function
|
||||||
@@ -334,3 +322,65 @@ func TestFilter(t *testing.T) {
|
|||||||
t.Fatalf("Expected %v, got %v", expected, result)
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test parse user
|
||||||
|
func TestParseUser(t *testing.T) {
|
||||||
|
t.Log("Testing parse user with a valid user")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
user := "user:pass:secret"
|
||||||
|
expected := types.User{
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
TotpSecret: "secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the parse user function
|
||||||
|
result, err := utils.ParseUser(user)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if !reflect.DeepEqual(expected, result) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing parse user with an escaped user")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
user = "user:p$$ass$$:secret"
|
||||||
|
expected = types.User{
|
||||||
|
Username: "user",
|
||||||
|
Password: "p$ass$",
|
||||||
|
TotpSecret: "secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the parse user function
|
||||||
|
result, err = utils.ParseUser(user)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing user: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the result is equal to the expected
|
||||||
|
if !reflect.DeepEqual(expected, result) {
|
||||||
|
t.Fatalf("Expected %v, got %v", expected, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Testing parse user with an invalid user")
|
||||||
|
|
||||||
|
// Create variables
|
||||||
|
user = "user::pass"
|
||||||
|
|
||||||
|
// Test the parse user function
|
||||||
|
_, err = utils.ParseUser(user)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error parsing user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user