mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-04 12:31:58 +00:00
refactor(security): move all database access into pkg/security/lookup
pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).
- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
parameter, JSON null arrays, expires_at timezone casts, passkey list
GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security/sectypes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MemoryProvider is an in-memory implementation of AuthProvider for testing/examples
|
||||
type MemoryProvider struct {
|
||||
mu sync.RWMutex
|
||||
secrets map[int]string // userID -> secret
|
||||
backupCodes map[int]map[string]bool // userID -> backup codes (code -> used)
|
||||
totpGen *Generator
|
||||
}
|
||||
|
||||
// NewMemoryProvider creates a new in-memory 2FA provider
|
||||
func NewMemoryProvider(config *Config) *MemoryProvider {
|
||||
if config == nil {
|
||||
config = DefaultConfig()
|
||||
}
|
||||
return &MemoryProvider{
|
||||
secrets: make(map[int]string),
|
||||
backupCodes: make(map[int]map[string]bool),
|
||||
totpGen: NewGenerator(config),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate2FASecret creates a new secret for a user
|
||||
func (m *MemoryProvider) Generate2FASecret(userID int, issuer, accountName string) (*sectypes.TwoFactorSecret, error) {
|
||||
secret, err := m.totpGen.GenerateSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qrURL := m.totpGen.GenerateQRCodeURL(secret, issuer, accountName)
|
||||
|
||||
backupCodes, err := GenerateBackupCodes(10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return §ypes.TwoFactorSecret{
|
||||
Secret: secret,
|
||||
QRCodeURL: qrURL,
|
||||
BackupCodes: backupCodes,
|
||||
Issuer: issuer,
|
||||
AccountName: accountName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate2FACode verifies a TOTP code
|
||||
func (m *MemoryProvider) Validate2FACode(secret string, code string) (bool, error) {
|
||||
return m.totpGen.ValidateCode(secret, code)
|
||||
}
|
||||
|
||||
// Enable2FA activates 2FA for a user
|
||||
func (m *MemoryProvider) Enable2FA(userID int, secret string, backupCodes []string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.secrets[userID] = secret
|
||||
|
||||
// Store backup codes
|
||||
if m.backupCodes[userID] == nil {
|
||||
m.backupCodes[userID] = make(map[string]bool)
|
||||
}
|
||||
|
||||
for _, code := range backupCodes {
|
||||
// Hash backup codes for security
|
||||
hash := sha256.Sum256([]byte(code))
|
||||
m.backupCodes[userID][hex.EncodeToString(hash[:])] = false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Disable2FA deactivates 2FA for a user
|
||||
func (m *MemoryProvider) Disable2FA(userID int) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
delete(m.secrets, userID)
|
||||
delete(m.backupCodes, userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get2FAStatus checks if user has 2FA enabled
|
||||
func (m *MemoryProvider) Get2FAStatus(userID int) (bool, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
_, exists := m.secrets[userID]
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// Get2FASecret retrieves the user's 2FA secret
|
||||
func (m *MemoryProvider) Get2FASecret(userID int) (string, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
secret, exists := m.secrets[userID]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("user does not have 2FA enabled")
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// GenerateBackupCodes creates backup codes for 2FA
|
||||
func (m *MemoryProvider) GenerateBackupCodes(userID int, count int) ([]string, error) {
|
||||
codes, err := GenerateBackupCodes(count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Clear old backup codes and store new ones
|
||||
m.backupCodes[userID] = make(map[string]bool)
|
||||
for _, code := range codes {
|
||||
hash := sha256.Sum256([]byte(code))
|
||||
m.backupCodes[userID][hex.EncodeToString(hash[:])] = false
|
||||
}
|
||||
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
// ValidateBackupCode checks and consumes a backup code
|
||||
func (m *MemoryProvider) ValidateBackupCode(userID int, code string) (bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
userCodes, exists := m.backupCodes[userID]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Hash the provided code
|
||||
hash := sha256.Sum256([]byte(code))
|
||||
hashStr := hex.EncodeToString(hash[:])
|
||||
|
||||
used, exists := userCodes[hashStr]
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if used {
|
||||
return false, fmt.Errorf("backup code already used")
|
||||
}
|
||||
|
||||
// Mark as used
|
||||
userCodes[hashStr] = true
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user