veza/veza-backend-api/internal/security/mfa.go
2025-12-03 20:29:37 +01:00

368 lines
9.3 KiB
Go

package security
import (
"crypto/rand"
"encoding/base32"
"fmt"
"time"
"github.com/pquerna/otp/totp"
)
// MFAMethod représente une méthode MFA
type MFAMethod struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Type string `json:"type"` // totp, sms, email, backup
Secret string `json:"secret,omitempty"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
IsActive bool `json:"is_active"`
IsVerified bool `json:"is_verified"`
CreatedAt time.Time `json:"created_at"`
VerifiedAt time.Time `json:"verified_at,omitempty"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
}
// MFASession représente une session MFA
type MFASession struct {
ID string `json:"id"`
UserID string `json:"user_id"`
MethodID string `json:"method_id"`
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
Used bool `json:"used"`
}
// MFAManager gère l'authentification multi-facteurs
type MFAManager struct {
methods map[string]*MFAMethod
sessions map[string]*MFASession
}
// NewMFAManager crée un nouveau gestionnaire MFA
func NewMFAManager() *MFAManager {
return &MFAManager{
methods: make(map[string]*MFAMethod),
sessions: make(map[string]*MFASession),
}
}
// GenerateTOTPSecret génère un secret TOTP
func (mfa *MFAManager) GenerateTOTPSecret(userID, email string) (*MFAMethod, error) {
// Générer un secret aléatoire
secret := make([]byte, 20)
if _, err := rand.Read(secret); err != nil {
return nil, fmt.Errorf("failed to generate secret: %w", err)
}
// Encoder en base32
secretBase32 := base32.StdEncoding.EncodeToString(secret)
// Créer la méthode TOTP
method := &MFAMethod{
ID: fmt.Sprintf("totp_%s", userID),
UserID: userID,
Type: "totp",
Secret: secretBase32,
IsActive: false,
IsVerified: false,
CreatedAt: time.Now(),
}
mfa.methods[method.ID] = method
return method, nil
}
// GenerateTOTPQRCode génère le QR code pour TOTP
func (mfa *MFAManager) GenerateTOTPQRCode(method *MFAMethod, issuer, accountName string) string {
// Format: otpauth://totp/issuer:account?secret=secret&issuer=issuer
url := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s",
issuer, accountName, method.Secret, issuer)
return url
}
// VerifyTOTP vérifie un code TOTP
func (mfa *MFAManager) VerifyTOTP(methodID, code string) (bool, error) {
method, exists := mfa.methods[methodID]
if !exists {
return false, fmt.Errorf("method not found")
}
if method.Type != "totp" {
return false, fmt.Errorf("method is not TOTP")
}
// Vérifier le code TOTP
valid := totp.Validate(code, method.Secret)
if valid {
method.LastUsedAt = time.Now()
if !method.IsVerified {
method.IsVerified = true
method.VerifiedAt = time.Now()
}
}
return valid, nil
}
// GenerateBackupCodes génère des codes de sauvegarde
func (mfa *MFAManager) GenerateBackupCodes(userID string, count int) ([]string, error) {
codes := make([]string, count)
for i := 0; i < count; i++ {
// Générer un code de 8 caractères
codeBytes := make([]byte, 4)
if _, err := rand.Read(codeBytes); err != nil {
return nil, fmt.Errorf("failed to generate backup code: %w", err)
}
// Encoder en base32 et prendre les 8 premiers caractères
code := base32.StdEncoding.EncodeToString(codeBytes)[:8]
codes[i] = code
}
// Créer la méthode de sauvegarde
method := &MFAMethod{
ID: fmt.Sprintf("backup_%s", userID),
UserID: userID,
Type: "backup",
Secret: "", // Les codes sont stockés séparément
IsActive: true,
IsVerified: true,
CreatedAt: time.Now(),
VerifiedAt: time.Now(),
}
mfa.methods[method.ID] = method
return codes, nil
}
// VerifyBackupCode vérifie un code de sauvegarde
func (mfa *MFAManager) VerifyBackupCode(userID, code string) (bool, error) {
methodID := fmt.Sprintf("backup_%s", userID)
method, exists := mfa.methods[methodID]
if !exists {
return false, fmt.Errorf("backup method not found")
}
// Dans un vrai système, les codes seraient stockés de manière sécurisée
// Ici on simule la vérification
valid := len(code) == 8 && method.IsActive
if valid {
method.LastUsedAt = time.Now()
}
return valid, nil
}
// GenerateSMSMFA génère une méthode MFA par SMS
func (mfa *MFAManager) GenerateSMSMFA(userID, phone string) (*MFAMethod, error) {
method := &MFAMethod{
ID: fmt.Sprintf("sms_%s", userID),
UserID: userID,
Type: "sms",
Phone: phone,
IsActive: false,
IsVerified: false,
CreatedAt: time.Now(),
}
mfa.methods[method.ID] = method
return method, nil
}
// SendSMSCode envoie un code SMS
func (mfa *MFAManager) SendSMSCode(methodID string) (string, error) {
method, exists := mfa.methods[methodID]
if !exists {
return "", fmt.Errorf("method not found")
}
if method.Type != "sms" {
return "", fmt.Errorf("method is not SMS")
}
// Générer un code à 6 chiffres
code := fmt.Sprintf("%06d", time.Now().UnixNano()%1000000)
// Dans un vrai système, on enverrait le SMS via un service
// Ici on simule l'envoi
fmt.Printf("SMS code sent to %s: %s\n", method.Phone, code)
return code, nil
}
// VerifySMSCode vérifie un code SMS
func (mfa *MFAManager) VerifySMSCode(methodID, code string) (bool, error) {
method, exists := mfa.methods[methodID]
if !exists {
return false, fmt.Errorf("method not found")
}
if method.Type != "sms" {
return false, fmt.Errorf("method is not SMS")
}
// Dans un vrai système, on vérifierait le code stocké
// Ici on simule la vérification
valid := len(code) == 6
if valid {
method.IsVerified = true
method.VerifiedAt = time.Now()
method.LastUsedAt = time.Now()
}
return valid, nil
}
// GenerateEmailMFA génère une méthode MFA par email
func (mfa *MFAManager) GenerateEmailMFA(userID, email string) (*MFAMethod, error) {
method := &MFAMethod{
ID: fmt.Sprintf("email_%s", userID),
UserID: userID,
Type: "email",
Email: email,
IsActive: false,
IsVerified: false,
CreatedAt: time.Now(),
}
mfa.methods[method.ID] = method
return method, nil
}
// SendEmailCode envoie un code par email
func (mfa *MFAManager) SendEmailCode(methodID string) (string, error) {
method, exists := mfa.methods[methodID]
if !exists {
return "", fmt.Errorf("method not found")
}
if method.Type != "email" {
return "", fmt.Errorf("method is not email")
}
// Générer un code à 6 chiffres
code := fmt.Sprintf("%06d", time.Now().UnixNano()%1000000)
// Dans un vrai système, on enverrait l'email via un service
// Ici on simule l'envoi
fmt.Printf("Email code sent to %s: %s\n", method.Email, code)
return code, nil
}
// VerifyEmailCode vérifie un code email
func (mfa *MFAManager) VerifyEmailCode(methodID, code string) (bool, error) {
method, exists := mfa.methods[methodID]
if !exists {
return false, fmt.Errorf("method not found")
}
if method.Type != "email" {
return false, fmt.Errorf("method is not email")
}
// Dans un vrai système, on vérifierait le code stocké
// Ici on simule la vérification
valid := len(code) == 6
if valid {
method.IsVerified = true
method.VerifiedAt = time.Now()
method.LastUsedAt = time.Now()
}
return valid, nil
}
// GetUserMFAMethods récupère toutes les méthodes MFA d'un utilisateur
func (mfa *MFAManager) GetUserMFAMethods(userID string) []*MFAMethod {
methods := make([]*MFAMethod, 0)
for _, method := range mfa.methods {
if method.UserID == userID {
methods = append(methods, method)
}
}
return methods
}
// ActivateMFAMethod active une méthode MFA
func (mfa *MFAManager) ActivateMFAMethod(methodID string) error {
method, exists := mfa.methods[methodID]
if !exists {
return fmt.Errorf("method not found")
}
if !method.IsVerified {
return fmt.Errorf("method must be verified before activation")
}
method.IsActive = true
return nil
}
// DeactivateMFAMethod désactive une méthode MFA
func (mfa *MFAManager) DeactivateMFAMethod(methodID string) error {
method, exists := mfa.methods[methodID]
if !exists {
return fmt.Errorf("method not found")
}
method.IsActive = false
return nil
}
// DeleteMFAMethod supprime une méthode MFA
func (mfa *MFAManager) DeleteMFAMethod(methodID string) error {
if _, exists := mfa.methods[methodID]; !exists {
return fmt.Errorf("method not found")
}
delete(mfa.methods, methodID)
return nil
}
// RequireMFA vérifie si un utilisateur doit utiliser MFA
func (mfa *MFAManager) RequireMFA(userID string) bool {
methods := mfa.GetUserMFAMethods(userID)
for _, method := range methods {
if method.IsActive && method.IsVerified {
return true
}
}
return false
}
// ValidateMFALogin valide une connexion MFA
func (mfa *MFAManager) ValidateMFALogin(userID, methodID, code string) (bool, error) {
method, exists := mfa.methods[methodID]
if !exists {
return false, fmt.Errorf("method not found")
}
if method.UserID != userID {
return false, fmt.Errorf("method does not belong to user")
}
if !method.IsActive || !method.IsVerified {
return false, fmt.Errorf("method is not active or verified")
}
switch method.Type {
case "totp":
return mfa.VerifyTOTP(methodID, code)
case "sms":
return mfa.VerifySMSCode(methodID, code)
case "email":
return mfa.VerifyEmailCode(methodID, code)
case "backup":
return mfa.VerifyBackupCode(userID, code)
default:
return false, fmt.Errorf("unsupported method type")
}
}