mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-10-20 13:04:06 +02:00
feat: Implement Two-Factor Authentication (2FA) functionality
- Added support for Two-Factor Authentication, including setup, verification, and disabling features. - Introduced new components for 2FA setup and verification, enhancing user security. - Updated user model to include fields for 2FA secret, status, and backup codes. - Created migration to add necessary database fields for 2FA. - Enhanced authentication handlers to manage 2FA processes during login and profile updates. - Updated routes to include 2FA-related endpoints for setup and verification.
This commit is contained in:
1 parent
ef4bee88b8
commit
a7d912aa44
14 files changed
+955
-18
No files matched your search
@@ -0,0 +1,158 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"strings"
|
||||
|
||||
// "github.com/pquerna/otp/base32"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
const (
|
||||
// IssuerName is the name of the issuer that appears in authenticator apps
|
||||
IssuerName = "GoMFT"
|
||||
// SecretSize is the size of the TOTP secret in bytes
|
||||
SecretSize = 20
|
||||
// BackupCodeCount is the number of backup codes to generate
|
||||
BackupCodeCount = 8
|
||||
// BackupCodeLength is the length of each backup code
|
||||
BackupCodeLength = 8
|
||||
)
|
||||
|
||||
// GenerateTOTPSecret generates a new TOTP secret for a user
|
||||
func GenerateTOTPSecret(email string) (string, string, error) {
|
||||
// Generate TOTP key using the library
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: IssuerName,
|
||||
AccountName: email,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate TOTP key: %v", err)
|
||||
}
|
||||
|
||||
// Generate QR code image
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(256, 256)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate QR code image: %v", err)
|
||||
}
|
||||
|
||||
// Encode image as PNG and convert to base64
|
||||
err = png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to encode QR code image: %v", err)
|
||||
}
|
||||
|
||||
// Create data URL
|
||||
dataURL := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
|
||||
return key.Secret(), dataURL, nil
|
||||
}
|
||||
|
||||
// ValidateTOTPCode validates a TOTP code against a secret
|
||||
func ValidateTOTPCode(secret string, code string) bool {
|
||||
// Remove any spaces from the code
|
||||
code = strings.ReplaceAll(code, " ", "")
|
||||
|
||||
// Use the library's Validate function
|
||||
return totp.Validate(code, secret)
|
||||
}
|
||||
|
||||
// GenerateBackupCodes generates a set of backup codes
|
||||
func GenerateBackupCodes() ([]string, error) {
|
||||
codes := make([]string, BackupCodeCount)
|
||||
for i := 0; i < BackupCodeCount; i++ {
|
||||
// Generate random bytes
|
||||
bytes := make([]byte, BackupCodeLength/2)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate backup code: %v", err)
|
||||
}
|
||||
|
||||
// Convert to hex string
|
||||
codes[i] = fmt.Sprintf("%x", bytes)
|
||||
}
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// ValidateBackupCode validates a backup code against a list of codes
|
||||
func ValidateBackupCode(providedCode string, storedCodes string) bool {
|
||||
if storedCodes == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove any spaces and convert to lowercase
|
||||
providedCode = strings.ToLower(strings.ReplaceAll(providedCode, " ", ""))
|
||||
|
||||
// Split stored codes
|
||||
codes := strings.Split(storedCodes, ",")
|
||||
|
||||
// Check if the provided code matches any stored code
|
||||
for _, code := range codes {
|
||||
if code == providedCode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// RemoveBackupCode removes a used backup code from the list
|
||||
func RemoveBackupCode(usedCode string, storedCodes string) string {
|
||||
if storedCodes == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
usedCode = strings.ToLower(strings.ReplaceAll(usedCode, " ", ""))
|
||||
codes := strings.Split(storedCodes, ",")
|
||||
|
||||
var newCodes []string
|
||||
for _, code := range codes {
|
||||
if code != usedCode {
|
||||
newCodes = append(newCodes, code)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(newCodes, ",")
|
||||
}
|
||||
|
||||
// GenerateQRCodeURL generates a QR code URL for an existing secret
|
||||
func GenerateQRCodeURL(secret string, email string) (string, error) {
|
||||
// Decode the base32 secret
|
||||
secretBytes, err := base32.StdEncoding.DecodeString(secret)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode secret: %v", err)
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: IssuerName,
|
||||
AccountName: email,
|
||||
Secret: secretBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate TOTP key: %v", err)
|
||||
}
|
||||
|
||||
// Generate QR code image
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(256, 256)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate QR code image: %v", err)
|
||||
}
|
||||
|
||||
// Encode image as PNG and convert to base64
|
||||
err = png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode QR code image: %v", err)
|
||||
}
|
||||
|
||||
// Create data URL
|
||||
dataURL := fmt.Sprintf("data:image/png;base64,%s", base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
|
||||
return dataURL, nil
|
||||
}
|
||||
@@ -25,6 +25,9 @@ type User struct {
|
||||
AccountLocked *bool `gorm:"default:false"`
|
||||
LockoutUntil *time.Time
|
||||
Theme string `gorm:"default:'light'"`
|
||||
TwoFactorSecret string `gorm:"type:varchar(32)"`
|
||||
TwoFactorEnabled bool `gorm:"default:false"`
|
||||
BackupCodes string `gorm:"type:text"` // Comma-separated backup codes
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Add2FA creates a migration for adding Two-Factor Authentication fields
|
||||
func Add2FA() *gormigrate.Migration {
|
||||
return &gormigrate.Migration{
|
||||
ID: "003_add_2fa",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Check if any tables exist (indicating an existing database)
|
||||
var count int64
|
||||
if err := tx.Raw("SELECT count(*) FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").Scan(&count).Error; err != nil {
|
||||
return fmt.Errorf("failed to check for existing tables: %v", err)
|
||||
}
|
||||
|
||||
// If tables exist, create a backup
|
||||
if count > 0 {
|
||||
// Get the database path
|
||||
sqlDB, err := tx.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying database: %v", err)
|
||||
}
|
||||
|
||||
var seq int
|
||||
var name, dbPath string
|
||||
if err := sqlDB.QueryRow("PRAGMA database_list").Scan(&seq, &name, &dbPath); err != nil {
|
||||
return fmt.Errorf("failed to get database path: %v", err)
|
||||
}
|
||||
|
||||
// Create backup file with timestamp
|
||||
backupFile := fmt.Sprintf("%s.backup.%s", dbPath, time.Now().Format("20060102_150405"))
|
||||
|
||||
// Read original database
|
||||
data, err := os.ReadFile(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read database for backup: %v", err)
|
||||
}
|
||||
|
||||
// Write backup
|
||||
if err := os.WriteFile(backupFile, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to create database backup: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created database backup at: %s\n", backupFile)
|
||||
}
|
||||
|
||||
// Add new columns for 2FA - one at a time for SQLite compatibility
|
||||
if err := tx.Exec(`ALTER TABLE users ADD COLUMN two_factor_secret VARCHAR(32)`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(`ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT FALSE`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(`ALTER TABLE users ADD COLUMN backup_codes TEXT`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
// Remove 2FA columns - one at a time for SQLite compatibility
|
||||
if err := tx.Exec(`ALTER TABLE users DROP COLUMN two_factor_secret`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(`ALTER TABLE users DROP COLUMN two_factor_enabled`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Exec(`ALTER TABLE users DROP COLUMN backup_codes`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ func InitMigrations(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
migrations := []*gormigrate.Migration{
|
||||
InitialSchema(),
|
||||
UpdateGDriveType(),
|
||||
Add2FA(),
|
||||
}
|
||||
|
||||
return gormigrate.New(db, gormigrate.DefaultOptions, migrations)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -56,11 +57,21 @@ func (h *Handlers) AuthMiddleware() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Safely extract claims with type assertions and defaults
|
||||
userID, _ := claims["user_id"].(float64)
|
||||
email, _ := claims["email"].(string)
|
||||
username, _ := claims["username"].(string)
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
|
||||
// Set user information in the context
|
||||
c.Set("userID", uint(claims["user_id"].(float64)))
|
||||
c.Set("email", claims["email"].(string))
|
||||
c.Set("username", claims["username"].(string))
|
||||
c.Set("isAdmin", claims["is_admin"].(bool))
|
||||
c.Set("userID", uint(userID))
|
||||
if email != "" {
|
||||
c.Set("email", email)
|
||||
}
|
||||
if username != "" {
|
||||
c.Set("username", username)
|
||||
}
|
||||
c.Set("isAdmin", isAdmin)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
@@ -142,10 +153,11 @@ func (h *Handlers) APIAdminMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// GenerateJWT generates a JWT token for the given user
|
||||
func (h *Handlers) GenerateJWT(userID uint, username string, isAdmin bool) (string, error) {
|
||||
func (h *Handlers) GenerateJWT(userID uint, email string, isAdmin bool) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"username": username,
|
||||
"email": email,
|
||||
"username": strings.Split(email, "@")[0], // Use email prefix as username
|
||||
"is_admin": isAdmin,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
@@ -243,24 +255,30 @@ func (h *Handlers) HandleLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token with all necessary user information
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"email": user.Email,
|
||||
"username": strings.Split(user.Email, "@")[0], // Use email prefix as username
|
||||
"is_admin": user.IsAdmin,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
// Check if 2FA is enabled
|
||||
if user.TwoFactorEnabled {
|
||||
// Store user ID temporarily for 2FA verification
|
||||
c.SetCookie("temp_user_id", fmt.Sprintf("%d", user.ID), 300, "/", "", false, true) // 5 minutes expiry
|
||||
|
||||
// Sign the token
|
||||
tokenString, err := token.SignedString([]byte(h.JWTSecret))
|
||||
// Redirect to 2FA verification page
|
||||
c.Redirect(http.StatusFound, "/login/verify")
|
||||
return
|
||||
}
|
||||
|
||||
// If 2FA is not enabled, proceed with normal login
|
||||
// Generate JWT token with all necessary user information
|
||||
isAdmin := false
|
||||
if user.IsAdmin != nil {
|
||||
isAdmin = *user.IsAdmin
|
||||
}
|
||||
token, err := h.GenerateJWT(user.ID, user.Email, isAdmin)
|
||||
if err != nil {
|
||||
components.Login(components.CreateTemplateContext(c), "Authentication error").Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Set token in cookie
|
||||
c.SetCookie("jwt_token", tokenString, 86400, "/", "", false, true)
|
||||
c.SetCookie("jwt_token", token, 86400, "/", "", false, true)
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
router.GET("/", h.HandleHome)
|
||||
router.GET("/login", h.HandleLoginPage)
|
||||
router.POST("/login", h.HandleLogin)
|
||||
router.GET("/login/verify", h.Handle2FAVerifyPage)
|
||||
router.POST("/login/verify", h.Handle2FAVerify)
|
||||
router.GET("/forgot-password", h.HandleForgotPasswordPage)
|
||||
router.POST("/forgot-password", h.HandleForgotPassword)
|
||||
router.GET("/reset-password", h.HandleResetPasswordPage)
|
||||
@@ -22,6 +24,11 @@ func (h *Handlers) RegisterRoutes(router *gin.Engine) {
|
||||
// Password change route - only accessed from profile page
|
||||
authorized.POST("/change-password", h.HandleChangePassword)
|
||||
|
||||
// 2FA routes - under profile
|
||||
authorized.GET("/profile/2fa/setup", h.Handle2FASetup)
|
||||
authorized.POST("/profile/2fa/verify", h.Handle2FAVerifySetup)
|
||||
authorized.POST("/profile/2fa/disable", h.Handle2FADisable)
|
||||
|
||||
{
|
||||
authorized.GET("/dashboard", h.HandleDashboard)
|
||||
authorized.GET("/configs", h.HandleConfigs)
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/starfleetcptn/gomft/components"
|
||||
"github.com/starfleetcptn/gomft/internal/auth"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Handle2FASetup handles the GET /profile/2fa/setup route
|
||||
func (h *Handlers) Handle2FASetup(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var user struct {
|
||||
Email string
|
||||
TwoFactorEnabled bool
|
||||
}
|
||||
|
||||
if err := h.DB.Table("users").Select("email, two_factor_enabled").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to get user")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if 2FA is already enabled
|
||||
if user.TwoFactorEnabled {
|
||||
c.Redirect(http.StatusFound, "/profile")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate TOTP secret and QR code URL
|
||||
secret, qrCodeURL, err := auth.GenerateTOTPSecret(user.Email)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate 2FA secret")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate backup codes
|
||||
backupCodes, err := auth.GenerateBackupCodes()
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate backup codes")
|
||||
return
|
||||
}
|
||||
|
||||
// Store secret and backup codes in session temporarily
|
||||
c.SetCookie("2fa_setup_secret", secret, 3600, "/", "", false, true)
|
||||
c.SetCookie("2fa_setup_backup_codes", strings.Join(backupCodes, ","), 3600, "/", "", false, true)
|
||||
|
||||
// Render setup page
|
||||
data := components.TwoFactorSetupData{
|
||||
QRCodeURL: qrCodeURL,
|
||||
Secret: secret,
|
||||
BackupCodes: backupCodes,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
components.TwoFactorSetup(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// Handle2FAVerifySetup handles the POST /profile/2fa/verify route
|
||||
func (h *Handlers) Handle2FAVerifySetup(c *gin.Context) {
|
||||
// Get user from context
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
var user struct {
|
||||
Email string
|
||||
}
|
||||
if err := h.DB.Table("users").Select("email").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to get user")
|
||||
return
|
||||
}
|
||||
|
||||
// Get secret from session
|
||||
secret, err := c.Cookie("2fa_setup_secret")
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "Setup session expired")
|
||||
return
|
||||
}
|
||||
|
||||
// Get backup codes from session
|
||||
backupCodes, err := c.Cookie("2fa_setup_backup_codes")
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "Setup session expired")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the code
|
||||
code := c.PostForm("code")
|
||||
if !auth.ValidateTOTPCode(secret, code) {
|
||||
// Regenerate QR code URL using the existing secret
|
||||
qrCodeURL, err := auth.GenerateQRCodeURL(secret, user.Email)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate QR code")
|
||||
return
|
||||
}
|
||||
|
||||
data := components.TwoFactorSetupData{
|
||||
QRCodeURL: qrCodeURL,
|
||||
Secret: secret,
|
||||
BackupCodes: strings.Split(backupCodes, ","),
|
||||
ErrorMessage: "Invalid verification code. Please try again.",
|
||||
}
|
||||
components.TwoFactorSetup(c.Request.Context(), data).Render(c, c.Writer)
|
||||
return
|
||||
}
|
||||
|
||||
// Update user with 2FA settings
|
||||
if err := h.DB.Table("users").Where("id = ?", userID).Updates(map[string]interface{}{
|
||||
"two_factor_secret": secret,
|
||||
"two_factor_enabled": true,
|
||||
"backup_codes": backupCodes,
|
||||
}).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to enable 2FA")
|
||||
return
|
||||
}
|
||||
|
||||
// Clear setup cookies
|
||||
c.SetCookie("2fa_setup_secret", "", -1, "/", "", false, true)
|
||||
c.SetCookie("2fa_setup_backup_codes", "", -1, "/", "", false, true)
|
||||
|
||||
// Redirect to profile with success message
|
||||
c.Redirect(http.StatusFound, "/profile?message=2FA+enabled+successfully")
|
||||
}
|
||||
|
||||
// Handle2FAVerifyPage handles the GET /login/verify route
|
||||
func (h *Handlers) Handle2FAVerifyPage(c *gin.Context) {
|
||||
// Check if we have a temporary user ID
|
||||
_, err := c.Cookie("temp_user_id")
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// Render verification page
|
||||
data := components.TwoFactorVerifyData{
|
||||
ErrorMessage: "",
|
||||
}
|
||||
components.TwoFactorVerify(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// Handle2FAVerify handles the POST /login/verify route
|
||||
func (h *Handlers) Handle2FAVerify(c *gin.Context) {
|
||||
// Get user ID from cookie
|
||||
tempUserID, err := c.Cookie("temp_user_id")
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse user ID
|
||||
var userID uint
|
||||
if _, err := fmt.Sscanf(tempUserID, "%d", &userID); err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
var user struct {
|
||||
TwoFactorSecret string
|
||||
BackupCodes string
|
||||
Email string
|
||||
IsAdmin *bool
|
||||
}
|
||||
if err := h.DB.Table("users").Select("two_factor_secret, backup_codes, email, is_admin").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
return
|
||||
}
|
||||
|
||||
code := c.PostForm("code")
|
||||
|
||||
// First try TOTP code
|
||||
if auth.ValidateTOTPCode(user.TwoFactorSecret, code) {
|
||||
// Generate new JWT token and set cookie
|
||||
isAdmin := false
|
||||
if user.IsAdmin != nil {
|
||||
isAdmin = *user.IsAdmin
|
||||
}
|
||||
token, err := h.GenerateJWT(userID, user.Email, isAdmin)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate token")
|
||||
return
|
||||
}
|
||||
c.SetCookie("jwt_token", token, 86400, "/", "", false, true)
|
||||
|
||||
// Clear temporary user ID cookie
|
||||
c.SetCookie("temp_user_id", "", -1, "/", "", false, true)
|
||||
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
// Then try backup code
|
||||
if auth.ValidateBackupCode(code, user.BackupCodes) {
|
||||
// Remove used backup code
|
||||
newBackupCodes := auth.RemoveBackupCode(code, user.BackupCodes)
|
||||
if err := h.DB.Model("users").Where("id = ?", userID).Update("backup_codes", newBackupCodes).Error; err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to update backup codes")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate new JWT token and set cookie
|
||||
isAdmin := false
|
||||
if user.IsAdmin != nil {
|
||||
isAdmin = *user.IsAdmin
|
||||
}
|
||||
token, err := h.GenerateJWT(userID, user.Email, isAdmin)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to generate token")
|
||||
return
|
||||
}
|
||||
c.SetCookie("jwt_token", token, 86400, "/", "", false, true)
|
||||
|
||||
// Clear temporary user ID cookie
|
||||
c.SetCookie("temp_user_id", "", -1, "/", "", false, true)
|
||||
|
||||
c.Redirect(http.StatusFound, "/dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
// If neither code is valid, show error
|
||||
data := components.TwoFactorVerifyData{
|
||||
ErrorMessage: "Invalid verification code. Please try again.",
|
||||
}
|
||||
components.TwoFactorVerify(c.Request.Context(), data).Render(c, c.Writer)
|
||||
}
|
||||
|
||||
// Handle2FADisable handles the POST /profile/2fa/disable route
|
||||
func (h *Handlers) Handle2FADisable(c *gin.Context) {
|
||||
// Get user ID from context
|
||||
userID := c.GetUint("userID")
|
||||
|
||||
// Get current password from form
|
||||
currentPassword := c.PostForm("current_password")
|
||||
if currentPassword == "" {
|
||||
c.Data(http.StatusBadRequest, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Current password is required</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
var user struct {
|
||||
PasswordHash string
|
||||
TwoFactorEnabled bool
|
||||
}
|
||||
if err := h.DB.Table("users").Select("password_hash, two_factor_enabled").Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.Data(http.StatusInternalServerError, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Failed to get user information</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(currentPassword)); err != nil {
|
||||
c.Data(http.StatusBadRequest, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Current password is incorrect</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if 2FA is already disabled
|
||||
if !user.TwoFactorEnabled {
|
||||
c.Data(http.StatusBadRequest, "text/html", []byte(`<div class="bg-yellow-100 border border-yellow-400 text-yellow-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Two-factor authentication is already disabled</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Disable 2FA
|
||||
if err := h.DB.Table("users").Where("id = ?", userID).Updates(map[string]interface{}{
|
||||
"two_factor_enabled": false,
|
||||
"two_factor_secret": nil,
|
||||
"backup_codes": nil,
|
||||
}).Error; err != nil {
|
||||
c.Data(http.StatusInternalServerError, "text/html", []byte(`<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Failed to disable two-factor authentication</span>
|
||||
</div>`))
|
||||
return
|
||||
}
|
||||
|
||||
// Return success message
|
||||
c.Data(http.StatusOK, "text/html", []byte(`<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4" role="alert">
|
||||
<span class="block sm:inline">Two-factor authentication has been disabled</span>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
</script>
|
||||
</div>`))
|
||||
}
|
||||
Reference in new issue
Block a user