refactor: use centralized config in auth service

This commit is contained in:
Stavros
2025-04-06 18:55:24 +03:00
parent 07ddd4f917
commit 5cf4e208c6
7 changed files with 252 additions and 200 deletions

View File

@@ -15,39 +15,24 @@ import (
"golang.org/x/crypto/bcrypt"
)
func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int, loginTimeout int, loginMaxRetries int) *Auth {
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
return &Auth{
Docker: docker,
Users: userList,
OAuthWhitelist: oauthWhitelist,
SessionExpiry: sessionExpiry,
LoginTimeout: loginTimeout,
LoginMaxRetries: loginMaxRetries,
LoginAttempts: make(map[string]*LoginAttempt),
Config: config,
Docker: docker,
LoginAttempts: make(map[string]*types.LoginAttempt),
}
}
// LoginAttempt tracks information about login attempts for rate limiting
type LoginAttempt struct {
FailedAttempts int
LastAttempt time.Time
LockedUntil time.Time
}
type Auth struct {
Users types.Users
Docker *docker.Docker
OAuthWhitelist []string
SessionExpiry int
LoginTimeout int
LoginMaxRetries int
LoginAttempts map[string]*LoginAttempt // Map of username/IP to login attempts
LoginMutex sync.RWMutex // Mutex to protect the LoginAttempts map
Config types.AuthConfig
Docker *docker.Docker
LoginAttempts map[string]*types.LoginAttempt
LoginMutex sync.RWMutex
}
func (auth *Auth) GetUser(username string) *types.User {
// Loop through users and return the user if the username matches
for _, user := range auth.Users {
for _, user := range auth.Config.Users {
if user.Username == username {
return &user
}
@@ -64,25 +49,25 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
auth.LoginMutex.RLock()
defer auth.LoginMutex.RUnlock()
// Return false if rate limiting is not configured
if auth.LoginMaxRetries <= 0 || auth.LoginTimeout <= 0 {
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return false, 0
}
// Check if the identifier exists in the map
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
return false, 0
}
// If account is locked, check if lock time has expired
if attempt.LockedUntil.After(time.Now()) {
// Calculate remaining lockout time in seconds
remaining := int(time.Until(attempt.LockedUntil).Seconds())
return true, remaining
}
// Lock has expired
return false, 0
}
@@ -90,48 +75,48 @@ func (auth *Auth) IsAccountLocked(identifier string) (bool, int) {
// RecordLoginAttempt records a login attempt for rate limiting
func (auth *Auth) RecordLoginAttempt(identifier string, success bool) {
// Skip if rate limiting is not configured
if auth.LoginMaxRetries <= 0 || auth.LoginTimeout <= 0 {
if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 {
return
}
auth.LoginMutex.Lock()
defer auth.LoginMutex.Unlock()
// Get current attempt record or create a new one
attempt, exists := auth.LoginAttempts[identifier]
if !exists {
attempt = &LoginAttempt{}
attempt = &types.LoginAttempt{}
auth.LoginAttempts[identifier] = attempt
}
// Update last attempt time
attempt.LastAttempt = time.Now()
// If successful login, reset failed attempts
if success {
attempt.FailedAttempts = 0
attempt.LockedUntil = time.Time{} // Reset lock time
return
}
// Increment failed attempts
attempt.FailedAttempts++
// If max retries reached, lock the account
if attempt.FailedAttempts >= auth.LoginMaxRetries {
attempt.LockedUntil = time.Now().Add(time.Duration(auth.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.LoginTimeout).Msg("Account locked due to too many failed login attempts")
if attempt.FailedAttempts >= auth.Config.LoginMaxRetries {
attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second)
log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts")
}
}
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
// If the whitelist is empty, allow all emails
if len(auth.OAuthWhitelist) == 0 {
if len(auth.Config.OauthWhitelist) == 0 {
return true
}
// Loop through the whitelist and return true if the email matches
for _, email := range auth.OAuthWhitelist {
for _, email := range auth.Config.OauthWhitelist {
if email == emailSrc {
return true
}
@@ -155,7 +140,7 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
if data.TotpPending {
sessionExpiry = 3600
} else {
sessionExpiry = auth.SessionExpiry
sessionExpiry = auth.Config.SessionExpiry
}
// Set data
@@ -228,7 +213,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie {
func (auth *Auth) UserAuthConfigured() bool {
// If there are users, return true
return len(auth.Users) > 0
return len(auth.Config.Users) > 0
}
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {

View File

@@ -8,26 +8,38 @@ import (
"tinyauth/internal/types"
)
var config = types.AuthConfig{
Users: types.Users{},
OauthWhitelist: []string{},
SessionExpiry: 3600,
}
func TestLoginRateLimiting(t *testing.T) {
// Initialize a new auth service with 3 max retries and 5 seconds timeout
authService := auth.NewAuth(&docker.Docker{}, types.Users{}, []string{}, 3600, 5, 3)
config.LoginMaxRetries = 3
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test identifier
identifier := "test_user"
// Test successful login - should not lock account
t.Log("Testing successful login")
authService.RecordLoginAttempt(identifier, true)
locked, _ := authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after successful login")
}
// Test 2 failed attempts - should not lock account yet
t.Log("Testing 2 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
authService.RecordLoginAttempt(identifier, false)
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked after only 2 failed attempts")
}
@@ -36,6 +48,7 @@ func TestLoginRateLimiting(t *testing.T) {
t.Log("Testing 3 failed login attempts")
authService.RecordLoginAttempt(identifier, false)
locked, remainingTime := authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked after reaching max retries")
}
@@ -45,32 +58,42 @@ func TestLoginRateLimiting(t *testing.T) {
// Test reset after waiting for timeout - use 1 second timeout for fast testing
t.Log("Testing unlocking after timeout")
// Create a new service for this test with very short timeout (1 second)
authService2 := auth.NewAuth(&docker.Docker{}, types.Users{}, []string{}, 3600, 1, 3)
// Reinitialize auth service with a shorter timeout for testing
config.LoginTimeout = 1
config.LoginMaxRetries = 3
authService = auth.NewAuth(config, &docker.Docker{})
// Add enough failed attempts to lock the account
for i := 0; i < 3; i++ {
authService2.RecordLoginAttempt(identifier, false)
authService.RecordLoginAttempt(identifier, false)
}
// Verify it's locked
locked, _ = authService2.IsAccountLocked(identifier)
locked, _ = authService.IsAccountLocked(identifier)
if !locked {
t.Fatalf("Account should be locked initially")
}
// Wait a bit and verify it gets unlocked after timeout
time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout
locked, _ = authService2.IsAccountLocked(identifier)
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should be unlocked after timeout period")
}
// Test disabled rate limiting
t.Log("Testing disabled rate limiting")
authDisabled := auth.NewAuth(&docker.Docker{}, types.Users{}, []string{}, 3600, 0, 0)
config.LoginMaxRetries = 0
config.LoginTimeout = 0
authService = auth.NewAuth(config, &docker.Docker{})
for i := 0; i < 10; i++ {
authDisabled.RecordLoginAttempt(identifier, false)
authService.RecordLoginAttempt(identifier, false)
}
locked, _ = authDisabled.IsAccountLocked(identifier)
locked, _ = authService.IsAccountLocked(identifier)
if locked {
t.Fatalf("Account should not be locked when rate limiting is disabled")
}
@@ -78,24 +101,26 @@ func TestLoginRateLimiting(t *testing.T) {
func TestConcurrentLoginAttempts(t *testing.T) {
// Initialize a new auth service with 2 max retries and 5 seconds timeout
authService := auth.NewAuth(&docker.Docker{}, types.Users{}, []string{}, 3600, 5, 2)
config.LoginMaxRetries = 2
config.LoginTimeout = 5
authService := auth.NewAuth(config, &docker.Docker{})
// Test multiple identifiers
identifiers := []string{"user1", "user2", "user3"}
// Test that locking one identifier doesn't affect others
t.Log("Testing multiple identifiers")
// Add enough failed attempts to lock first user (2 attempts with maxRetries=2)
authService.RecordLoginAttempt(identifiers[0], false)
authService.RecordLoginAttempt(identifiers[0], false)
// Check if first user is locked
locked, _ := authService.IsAccountLocked(identifiers[0])
if !locked {
t.Fatalf("User1 should be locked after reaching max retries")
}
// Check that other users are not affected
for i := 1; i < len(identifiers); i++ {
locked, _ := authService.IsAccountLocked(identifiers[i])
@@ -106,11 +131,13 @@ func TestConcurrentLoginAttempts(t *testing.T) {
// Test successful login after failed attempts (but before lock)
t.Log("Testing successful login after failed attempts but before lock")
// One failed attempt for user2
authService.RecordLoginAttempt(identifiers[1], false)
// Successful login should reset the counter
authService.RecordLoginAttempt(identifiers[1], true)
// Now try a failed login again - should not be locked as counter was reset
authService.RecordLoginAttempt(identifiers[1], false)
locked, _ = authService.IsAccountLocked(identifiers[1])