mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-01 15:34:25 +00:00
feat(security): ✨ Add two-factor authentication support
* Implement TwoFactorAuthenticator for 2FA login. * Create DatabaseTwoFactorProvider for PostgreSQL integration. * Add MemoryTwoFactorProvider for in-memory testing. * Develop TOTPGenerator for generating and validating codes. * Include tests for all new functionalities. * Ensure backup codes are securely hashed and validated.
This commit is contained in:
399
pkg/security/totp_integration_test.go
Normal file
399
pkg/security/totp_integration_test.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package security_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
var ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
|
||||
// MockAuthenticator is a simple authenticator for testing 2FA
|
||||
type MockAuthenticator struct {
|
||||
users map[string]*security.UserContext
|
||||
}
|
||||
|
||||
func NewMockAuthenticator() *MockAuthenticator {
|
||||
return &MockAuthenticator{
|
||||
users: map[string]*security.UserContext{
|
||||
"testuser": {
|
||||
UserID: 1,
|
||||
UserName: "testuser",
|
||||
Email: "test@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||
user, exists := m.users[req.Username]
|
||||
if !exists || req.Password != "password" {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
return &security.LoginResponse{
|
||||
Token: "mock-token",
|
||||
RefreshToken: "mock-refresh-token",
|
||||
User: user,
|
||||
ExpiresIn: 3600,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
return m.users["testuser"], nil
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Setup(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup 2FA
|
||||
secret, err := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Setup2FA() error = %v", err)
|
||||
}
|
||||
|
||||
if secret.Secret == "" {
|
||||
t.Error("Setup2FA() returned empty secret")
|
||||
}
|
||||
|
||||
if secret.QRCodeURL == "" {
|
||||
t.Error("Setup2FA() returned empty QR code URL")
|
||||
}
|
||||
|
||||
if len(secret.BackupCodes) == 0 {
|
||||
t.Error("Setup2FA() returned no backup codes")
|
||||
}
|
||||
|
||||
if secret.Issuer != "TestApp" {
|
||||
t.Errorf("Setup2FA() Issuer = %s, want TestApp", secret.Issuer)
|
||||
}
|
||||
|
||||
if secret.AccountName != "test@example.com" {
|
||||
t.Errorf("Setup2FA() AccountName = %s, want test@example.com", secret.AccountName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Enable2FA(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup 2FA
|
||||
secret, err := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Setup2FA() error = %v", err)
|
||||
}
|
||||
|
||||
// Generate valid code
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, err := totp.GenerateCode(secret.Secret, time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateCode() error = %v", err)
|
||||
}
|
||||
|
||||
// Enable 2FA with valid code
|
||||
err = tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
if err != nil {
|
||||
t.Errorf("Enable2FA() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify 2FA is enabled
|
||||
status, err := provider.Get2FAStatus(1)
|
||||
if err != nil {
|
||||
t.Fatalf("Get2FAStatus() error = %v", err)
|
||||
}
|
||||
|
||||
if !status {
|
||||
t.Error("Enable2FA() did not enable 2FA")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Enable2FA_InvalidCode(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup 2FA
|
||||
secret, err := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Setup2FA() error = %v", err)
|
||||
}
|
||||
|
||||
// Try to enable with invalid code
|
||||
err = tfaAuth.Enable2FA(1, secret.Secret, "000000")
|
||||
if err == nil {
|
||||
t.Error("Enable2FA() should fail with invalid code")
|
||||
}
|
||||
|
||||
// Verify 2FA is not enabled
|
||||
status, _ := provider.Get2FAStatus(1)
|
||||
if status {
|
||||
t.Error("Enable2FA() should not enable 2FA with invalid code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Login_Without2FA(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
|
||||
if resp.Requires2FA {
|
||||
t.Error("Login() should not require 2FA when not enabled")
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
t.Error("Login() should return token when 2FA not required")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Login_With2FA_NoCode(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Try to login without 2FA code
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
|
||||
if !resp.Requires2FA {
|
||||
t.Error("Login() should require 2FA when enabled")
|
||||
}
|
||||
|
||||
if resp.Token != "" {
|
||||
t.Error("Login() should not return token when 2FA required but not provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Login_With2FA_ValidCode(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Generate new valid code for login
|
||||
newCode, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
|
||||
// Login with 2FA code
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: newCode,
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
|
||||
if resp.Requires2FA {
|
||||
t.Error("Login() should not require 2FA when valid code provided")
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
t.Error("Login() should return token when 2FA validated")
|
||||
}
|
||||
|
||||
if !resp.User.TwoFactorEnabled {
|
||||
t.Error("Login() should set TwoFactorEnabled on user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Login_With2FA_InvalidCode(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Try to login with invalid code
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: "000000",
|
||||
}
|
||||
|
||||
_, err := tfaAuth.Login(context.Background(), req)
|
||||
if err == nil {
|
||||
t.Error("Login() should fail with invalid 2FA code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Login_WithBackupCode(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Get backup codes
|
||||
backupCodes, _ := tfaAuth.RegenerateBackupCodes(1, 10)
|
||||
|
||||
// Login with backup code
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: backupCodes[0],
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() with backup code error = %v", err)
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
t.Error("Login() should return token when backup code validated")
|
||||
}
|
||||
|
||||
// Try to use same backup code again
|
||||
req2 := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: backupCodes[0],
|
||||
}
|
||||
|
||||
_, err = tfaAuth.Login(context.Background(), req2)
|
||||
if err == nil {
|
||||
t.Error("Login() should fail when reusing backup code")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_Disable2FA(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Disable 2FA
|
||||
err := tfaAuth.Disable2FA(1)
|
||||
if err != nil {
|
||||
t.Errorf("Disable2FA() error = %v", err)
|
||||
}
|
||||
|
||||
// Verify 2FA is disabled
|
||||
status, _ := provider.Get2FAStatus(1)
|
||||
if status {
|
||||
t.Error("Disable2FA() did not disable 2FA")
|
||||
}
|
||||
|
||||
// Login should not require 2FA
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
|
||||
if resp.Requires2FA {
|
||||
t.Error("Login() should not require 2FA after disabling")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoFactorAuthenticator_RegenerateBackupCodes(t *testing.T) {
|
||||
baseAuth := NewMockAuthenticator()
|
||||
provider := security.NewMemoryTwoFactorProvider(nil)
|
||||
tfaAuth := security.NewTwoFactorAuthenticator(baseAuth, provider, nil)
|
||||
|
||||
// Setup and enable 2FA
|
||||
secret, _ := tfaAuth.Setup2FA(1, "TestApp", "test@example.com")
|
||||
totp := security.NewTOTPGenerator(nil)
|
||||
code, _ := totp.GenerateCode(secret.Secret, time.Now())
|
||||
tfaAuth.Enable2FA(1, secret.Secret, code)
|
||||
|
||||
// Get initial backup codes
|
||||
codes1, err := tfaAuth.RegenerateBackupCodes(1, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("RegenerateBackupCodes() error = %v", err)
|
||||
}
|
||||
|
||||
if len(codes1) != 10 {
|
||||
t.Errorf("RegenerateBackupCodes() returned %d codes, want 10", len(codes1))
|
||||
}
|
||||
|
||||
// Regenerate backup codes
|
||||
codes2, err := tfaAuth.RegenerateBackupCodes(1, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("RegenerateBackupCodes() error = %v", err)
|
||||
}
|
||||
|
||||
// Old codes should not work
|
||||
req := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: codes1[0],
|
||||
}
|
||||
|
||||
_, err = tfaAuth.Login(context.Background(), req)
|
||||
if err == nil {
|
||||
t.Error("Login() should fail with old backup code after regeneration")
|
||||
}
|
||||
|
||||
// New codes should work
|
||||
req2 := security.LoginRequest{
|
||||
Username: "testuser",
|
||||
Password: "password",
|
||||
TwoFactorCode: codes2[0],
|
||||
}
|
||||
|
||||
resp, err := tfaAuth.Login(context.Background(), req2)
|
||||
if err != nil {
|
||||
t.Fatalf("Login() with new backup code error = %v", err)
|
||||
}
|
||||
|
||||
if resp.Token == "" {
|
||||
t.Error("Login() should return token with new backup code")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user