206 lines
6.7 KiB
Go
206 lines
6.7 KiB
Go
package v1
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gochat/gochat/internal/auth"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
// Reference: P2E §1.5 — MFA HTTP handlers
|
|
// Maps to Chatwoot enterprise TwoFactorAuthController:
|
|
// - enable → POST /api/v1/auth/mfa/enable (generates secret + QR URI)
|
|
// - verify → POST /api/v1/auth/mfa/verify (validates TOTP code, enables MFA)
|
|
// - disable → POST /api/v1/auth/mfa/disable (disables MFA after code verification)
|
|
|
|
// MFAHandler handles MFA (TOTP) HTTP endpoints.
|
|
type MFAHandler struct {
|
|
mfaService *auth.MFAService
|
|
}
|
|
|
|
// NewMFAHandler creates a MFA handler with service dependency.
|
|
func NewMFAHandler(mfaService *auth.MFAService) *MFAHandler {
|
|
return &MFAHandler{
|
|
mfaService: mfaService,
|
|
}
|
|
}
|
|
|
|
// --- Request/Response structs ---
|
|
|
|
// EnableMFARequest is the JSON body for MFA enablement initiation.
|
|
type EnableMFARequest struct {
|
|
// No body required — user_id comes from auth context
|
|
}
|
|
|
|
// EnableMFAResponse is the JSON response for MFA enablement initiation.
|
|
type EnableMFAResponse struct {
|
|
TOTPSecret string `json:"totp_secret"` // base32 secret for manual entry
|
|
QRURI string `json:"qr_uri"` // otpauth:// URI for QR code generation
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// VerifyMFARequest is the JSON body for MFA TOTP verification.
|
|
type VerifyMFARequest struct {
|
|
TOTPSecret string `json:"totp_secret" binding:"required"` // secret from enable step
|
|
TOTPCode string `json:"totp_code" binding:"required"` // 6-digit code from authenticator app
|
|
}
|
|
|
|
// DisableMFARequest is the JSON body for MFA disablement.
|
|
type DisableMFARequest struct {
|
|
TOTPCode string `json:"totp_code" binding:"required"` // current TOTP code for verification
|
|
}
|
|
|
|
// --- Handlers ---
|
|
|
|
// EnableMFA initiates MFA setup: generates a TOTP secret and QR URI.
|
|
// POST /api/v1/auth/mfa/enable
|
|
// Requires authentication — uses user_id from JWT context.
|
|
// The user must verify a TOTP code before MFA is actually activated.
|
|
func (h *MFAHandler) EnableMFA(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
// Check if MFA is already enabled
|
|
enabled, err := h.mfaService.IsMFAEnabled(userID)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
if enabled {
|
|
response.AbortWithStatusError(c, http.StatusConflict, response.ErrConflict, "MFA is already enabled for this user")
|
|
return
|
|
}
|
|
|
|
// Generate new TOTP secret + QR URI
|
|
secret, qrURI, err := h.mfaService.GenerateTOTPSecret(userID)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, EnableMFAResponse{
|
|
TOTPSecret: secret,
|
|
QRURI: qrURI,
|
|
Message: "Scan QR code with your authenticator app, then verify with a TOTP code",
|
|
})
|
|
}
|
|
|
|
// VerifyMFA completes MFA setup: verifies TOTP code and enables MFA on the user.
|
|
// POST /api/v1/auth/mfa/verify
|
|
// Requires authentication — uses user_id from JWT context.
|
|
// This is the second step: user provides secret + code from authenticator app.
|
|
func (h *MFAHandler) VerifyMFA(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
var req VerifyMFARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
// Validate the TOTP code against the provided secret
|
|
cfg := auth.DefaultTOTPConfig()
|
|
if !auth.ValidateTOTPCode(req.TOTPSecret, req.TOTPCode, cfg) {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "Invalid TOTP code, please try again")
|
|
return
|
|
}
|
|
|
|
// Enable TOTP on the user (stores secret in DB)
|
|
if err := h.mfaService.EnableTOTP(userID, req.TOTPSecret); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"message": "MFA enabled successfully",
|
|
"mfa_enabled": true,
|
|
})
|
|
}
|
|
|
|
// DisableMFA disables MFA after verifying the current TOTP code.
|
|
// POST /api/v1/auth/mfa/disable
|
|
// Requires authentication — uses user_id from JWT context.
|
|
func (h *MFAHandler) DisableMFA(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
var req DisableMFARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
|
return
|
|
}
|
|
|
|
// Disable TOTP (requires valid current code for security)
|
|
if err := h.mfaService.DisableTOTP(userID, req.TOTPCode); err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"message": "MFA disabled successfully",
|
|
"mfa_enabled": false,
|
|
})
|
|
}
|
|
|
|
// MFAStatus returns the current MFA status for the authenticated user.
|
|
// GET /api/v1/auth/mfa/status
|
|
func (h *MFAHandler) MFAStatus(c *gin.Context) {
|
|
userID := c.GetUint("user_id")
|
|
if userID == 0 {
|
|
response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "Authentication required")
|
|
return
|
|
}
|
|
|
|
enabled, err := h.mfaService.IsMFAEnabled(userID)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, err.Error())
|
|
return
|
|
}
|
|
|
|
response.OK(c, gin.H{
|
|
"mfa_enabled": enabled,
|
|
})
|
|
}
|
|
|
|
// RegisterMFARoutes sets up MFA routes on a Gin router group.
|
|
// These routes require authentication (AuthRequired middleware).
|
|
func RegisterMFARoutes(rg *gin.RouterGroup, handler *MFAHandler) {
|
|
mfaGroup := rg.Group("/auth/mfa")
|
|
{
|
|
mfaGroup.POST("/enable", handler.EnableMFA)
|
|
mfaGroup.POST("/verify", handler.VerifyMFA)
|
|
mfaGroup.POST("/disable", handler.DisableMFA)
|
|
mfaGroup.GET("/status", handler.MFAStatus)
|
|
mfaGroup.POST("/backup_codes", handler.BackupCodes)
|
|
}
|
|
}
|
|
|
|
// BackupCodes generates one-time MFA backup codes.
|
|
// POST /api/v1/profile/mfa/backup_codes or /api/v1/auth/mfa/backup_codes
|
|
// Reference: Chatwoot MfaController#backup_codes
|
|
func (h *MFAHandler) BackupCodes(c *gin.Context) {
|
|
userID := getUserID(c)
|
|
if userID == 0 {
|
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
|
return
|
|
}
|
|
codes, err := h.mfaService.GenerateBackupCodes(userID)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"backup_codes": codes})
|
|
}
|