Account Security¶
SystemForge provides security features for identity management including account lockout protection against brute-force attacks.
Account Lockout¶
The lockout package protects against brute-force password attacks by temporarily locking accounts after repeated failed login attempts.
Features¶
- Configurable max attempts and lockout duration
- Sliding window for attempt tracking
- Memory and Redis storage backends
- Manual lock/unlock capabilities
- Automatic expired lock cleanup
Quick Start¶
import (
"github.com/grokify/systemforge/identity/security"
)
// Create a memory store for development
store := security.NewMemoryLockoutStore()
// Create the lockout service
lockout := security.NewLockout(store,
security.WithMaxAttempts(5),
security.WithLockoutDuration(15*time.Minute),
)
defer lockout.Close()
// In your login handler
func handleLogin(ctx context.Context, email, password string) error {
// Check and record in one call (recommended)
success := validateCredentials(email, password)
err := lockout.CheckAndRecord(ctx, email, success)
if err != nil {
if errors.Is(err, security.ErrAccountLocked) {
return fmt.Errorf("account locked, try again later")
}
return err
}
if !success {
return fmt.Errorf("invalid credentials")
}
// Login successful
return nil
}
Configuration¶
Lockout Options¶
| Option | Description | Default |
|---|---|---|
WithMaxAttempts(n) |
Failed attempts before lockout | 5 |
WithLockoutDuration(d) |
How long account stays locked | 15 minutes |
WithLockoutConfig(cfg) |
Full configuration struct | See below |
Config Struct¶
cfg := security.LockoutConfig{
MaxAttempts: 5, // Lock after 5 failures
LockoutDuration: 15 * time.Minute, // Lock for 15 minutes
AttemptWindow: 15 * time.Minute, // Only count recent attempts
CleanupInterval: 5 * time.Minute, // Clean up old data every 5 min
}
lockout := security.NewLockout(store, security.WithLockoutConfig(cfg))
Storage Backends¶
Memory Store¶
For development and single-instance deployments:
Redis Store¶
For production and multi-instance deployments:
import "github.com/redis/go-redis/v9"
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
store := security.NewRedisLockoutStore(client,
security.WithRedisKeyPrefix("myapp:lockout:"),
)
API Reference¶
Checking Lock Status¶
// Check if an account is currently locked
locked, err := lockout.IsLocked(ctx, identifier)
// Get detailed status
status, err := lockout.GetStatus(ctx, identifier)
fmt.Printf("Locked: %v\n", status.IsLocked)
fmt.Printf("Failed attempts: %d\n", status.FailedAttempts)
fmt.Printf("Remaining attempts: %d\n", status.RemainingAttempts)
fmt.Printf("Locked until: %v\n", status.LockedUntil)
Recording Attempts¶
// Record a failed attempt (may trigger lockout)
err := lockout.RecordFailure(ctx, identifier)
if errors.Is(err, security.ErrAccountLocked) {
// Account is now locked
}
// Record a successful login (resets attempt counter)
err := lockout.RecordSuccess(ctx, identifier)
// Combined check-and-record (recommended for login flows)
err := lockout.CheckAndRecord(ctx, identifier, success)
Manual Lock Management¶
// Manually lock an account (e.g., admin action)
err := lockout.Lock(ctx, identifier, time.Now().Add(24*time.Hour))
// Manually unlock an account
err := lockout.Unlock(ctx, identifier)
// Reset all lockout state
err := lockout.Reset(ctx, identifier)
Lockout Status¶
The LockoutStatus struct contains:
type LockoutStatus struct {
IsLocked bool // Account currently locked
FailedAttempts int // Failed attempts in window
RemainingAttempts int // Attempts before lockout
LockedUntil time.Time // When lock expires
LastAttempt time.Time // Last failed attempt time
}
Best Practices¶
-
Use email as identifier: Lock by email/username, not by IP (IPs can be shared)
-
Show remaining attempts: Help legitimate users avoid lockout
-
Combine with rate limiting: Add IP-based rate limiting for defense in depth
-
Log lockout events: Monitor for attack patterns
-
Provide recovery path: Allow password reset to unlock accounts
Errors¶
| Error | Description |
|---|---|
ErrAccountLocked |
Account is currently locked |
ErrStorageFailure |
Storage backend error |
ErrInvalidThreshold |
Invalid configuration value |