Junie: add TOTP authentication
This commit is contained in:
180
cmd/mcias/totp.go
Normal file
180
cmd/mcias/totp.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/data"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
totpUsername string
|
||||
totpCode string
|
||||
)
|
||||
|
||||
var totpCmd = &cobra.Command{
|
||||
Use: "totp",
|
||||
Short: "Manage TOTP authentication",
|
||||
Long: `Commands for managing TOTP (Time-based One-Time Password) authentication in the MCIAS system.`,
|
||||
}
|
||||
|
||||
var enableTOTPCmd = &cobra.Command{
|
||||
Use: "enable",
|
||||
Short: "Enable TOTP for a user",
|
||||
Long: `Enable TOTP (Time-based One-Time Password) authentication for a user in the MCIAS system.
|
||||
This command requires a username.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
enableTOTP()
|
||||
},
|
||||
}
|
||||
|
||||
var validateTOTPCmd = &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Validate a TOTP code",
|
||||
Long: `Validate a TOTP code for a user in the MCIAS system.
|
||||
This command requires a username and a TOTP code.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
validateTOTP()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(totpCmd)
|
||||
totpCmd.AddCommand(enableTOTPCmd)
|
||||
totpCmd.AddCommand(validateTOTPCmd)
|
||||
|
||||
enableTOTPCmd.Flags().StringVarP(&totpUsername, "username", "u", "", "Username to enable TOTP for")
|
||||
if err := enableTOTPCmd.MarkFlagRequired("username"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
|
||||
}
|
||||
|
||||
validateTOTPCmd.Flags().StringVarP(&totpUsername, "username", "u", "", "Username to validate TOTP code for")
|
||||
validateTOTPCmd.Flags().StringVarP(&totpCode, "code", "c", "", "TOTP code to validate")
|
||||
if err := validateTOTPCmd.MarkFlagRequired("username"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking username flag as required: %v\n", err)
|
||||
}
|
||||
if err := validateTOTPCmd.MarkFlagRequired("code"); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error marking code flag as required: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func enableTOTP() {
|
||||
dbPath := viper.GetString("db")
|
||||
|
||||
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
|
||||
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Get the user from the database
|
||||
var userID string
|
||||
var created int64
|
||||
var username string
|
||||
var password, salt []byte
|
||||
var totpSecret sql.NullString
|
||||
|
||||
query := `SELECT id, created, user, password, salt, totp_secret FROM users WHERE user = ?`
|
||||
err = db.QueryRow(query, totpUsername).Scan(&userID, &created, &username, &password, &salt, &totpSecret)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
logger.Fatalf("User %s does not exist", totpUsername)
|
||||
}
|
||||
logger.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
// Check if TOTP is already enabled
|
||||
if totpSecret.Valid && totpSecret.String != "" {
|
||||
logger.Fatalf("TOTP is already enabled for user %s", totpUsername)
|
||||
}
|
||||
|
||||
// Create a user object
|
||||
user := &data.User{
|
||||
ID: userID,
|
||||
Created: created,
|
||||
User: username,
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
}
|
||||
|
||||
// Generate a TOTP secret
|
||||
secret, err := user.GenerateTOTPSecret()
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to generate TOTP secret: %v", err)
|
||||
}
|
||||
|
||||
// Update the user in the database
|
||||
updateQuery := `UPDATE users SET totp_secret = ? WHERE id = ?`
|
||||
_, err = db.Exec(updateQuery, secret, userID)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to update user: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("TOTP enabled for user %s\n", totpUsername)
|
||||
fmt.Printf("Secret: %s\n", secret)
|
||||
fmt.Println("Please save this secret in your authenticator app.")
|
||||
fmt.Println("You will need to provide a TOTP code when logging in.")
|
||||
}
|
||||
|
||||
func validateTOTP() {
|
||||
dbPath := viper.GetString("db")
|
||||
|
||||
logger := log.New(os.Stdout, "MCIAS: ", log.LstdFlags)
|
||||
|
||||
db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to open database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Get the user from the database
|
||||
var userID string
|
||||
var created int64
|
||||
var username string
|
||||
var password, salt []byte
|
||||
var totpSecret sql.NullString
|
||||
|
||||
query := `SELECT id, created, user, password, salt, totp_secret FROM users WHERE user = ?`
|
||||
err = db.QueryRow(query, totpUsername).Scan(&userID, &created, &username, &password, &salt, &totpSecret)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
logger.Fatalf("User %s does not exist", totpUsername)
|
||||
}
|
||||
logger.Fatalf("Failed to get user: %v", err)
|
||||
}
|
||||
|
||||
// Check if TOTP is enabled
|
||||
if !totpSecret.Valid || totpSecret.String == "" {
|
||||
logger.Fatalf("TOTP is not enabled for user %s", totpUsername)
|
||||
}
|
||||
|
||||
// Create a user object
|
||||
user := &data.User{
|
||||
ID: userID,
|
||||
Created: created,
|
||||
User: username,
|
||||
Password: password,
|
||||
Salt: salt,
|
||||
TOTPSecret: totpSecret.String,
|
||||
}
|
||||
|
||||
// Validate the TOTP code
|
||||
valid, err := user.ValidateTOTPCode(totpCode)
|
||||
if err != nil {
|
||||
logger.Fatalf("Failed to validate TOTP code: %v", err)
|
||||
}
|
||||
|
||||
if valid {
|
||||
fmt.Println("TOTP code is valid")
|
||||
} else {
|
||||
fmt.Println("TOTP code is invalid")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user