Account Lockout
Configurable failed-attempt tracking and automatic account lockout to defend against brute-force attacks.
Authsome's account lockout system automatically suspends authentication for a user after too many consecutive failed sign-in attempts. When a lockout is triggered, further sign-in attempts are rejected regardless of whether the credentials are correct. This prevents brute-force and credential-stuffing attacks without requiring you to write custom rate-limiting code.
How it works
On every failed authentication attempt (wrong password, expired MFA code, etc.), Authsome increments a failure counter for the user. When the counter reaches MaxAttempts, the account is locked for LockoutDuration. The counter resets to zero after ResetAfter without a new failure, so a single forgotten password doesn't permanently affect a user who guesses correctly on attempt N.
Attempt 1 (fail): counter = 1
Attempt 2 (fail): counter = 2
Attempt 3 (fail): counter = 3 → MaxAttempts reached → lock for LockoutDuration
Attempt 4 (success during lockout): rejected with ErrAccountLocked
...
After LockoutDuration: lock lifted, counter reset to 0Configuration
import (
"github.com/xraph/authsome"
"github.com/xraph/authsome/lockout"
)
auth := authsome.New(
authsome.WithLockout(lockout.Config{
MaxAttempts: 5,
LockoutDuration: 15 * time.Minute,
ResetAfter: 1 * time.Hour,
}),
)| Field | Type | Default | Description |
|---|---|---|---|
MaxAttempts | int | 5 | Number of consecutive failures before locking the account |
LockoutDuration | time.Duration | 15m | How long the account stays locked after MaxAttempts is reached |
ResetAfter | time.Duration | 1h | How long without a failure before the counter resets to zero |
Enabled | bool | true | Set to false to disable lockout globally |
WhitelistIPs | []string | nil | IP addresses exempt from lockout (e.g., internal health check IPs) |
Recommended production settings
For a general-purpose consumer application:
lockout.Config{
MaxAttempts: 5,
LockoutDuration: 30 * time.Minute,
ResetAfter: 2 * time.Hour,
}For a high-security internal application:
lockout.Config{
MaxAttempts: 3,
LockoutDuration: 24 * time.Hour,
ResetAfter: 4 * time.Hour,
}The LockoutTracker interface
Authsome tracks lockout state through the LockoutTracker interface. You can replace the built-in tracker with your own implementation backed by Redis, a database, or any other store.
type LockoutTracker interface {
// Increment increments the failure counter for the user and returns the new count.
Increment(ctx context.Context, userID string) (int, error)
// Get returns the current failure count and whether the account is currently locked.
Get(ctx context.Context, userID string) (count int, lockedUntil *time.Time, err error)
// Lock locks the account for the given duration.
Lock(ctx context.Context, userID string, until time.Time) error
// Reset clears the failure counter and any active lock for the user.
Reset(ctx context.Context, userID string) error
}Built-in: memory tracker
The memory tracker is the default. It stores state in a Go map protected by a sync.RWMutex. It is suitable for single-instance applications, testing, and development.
import "github.com/xraph/authsome/lockout"
tracker := lockout.NewMemoryTracker()
auth := authsome.New(
authsome.WithLockoutTracker(tracker),
authsome.WithLockout(lockout.Config{
MaxAttempts: 5,
LockoutDuration: 15 * time.Minute,
ResetAfter: 1 * time.Hour,
}),
)The memory tracker does not persist state across restarts. An attacker can bypass lockout by triggering a process restart. For production deployments, use a Redis or database-backed tracker.
Custom tracker: Redis example
package redislockout
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"github.com/xraph/authsome/lockout"
)
type RedisTracker struct {
client *redis.Client
reset time.Duration
}
func New(client *redis.Client, resetAfter time.Duration) lockout.LockoutTracker {
return &RedisTracker{client: client, reset: resetAfter}
}
func (r *RedisTracker) Increment(ctx context.Context, userID string) (int, error) {
key := fmt.Sprintf("lockout:count:%s", userID)
count, err := r.client.Incr(ctx, key).Result()
if err != nil {
return 0, err
}
// Set expiry only on first increment (reset window).
if count == 1 {
r.client.Expire(ctx, key, r.reset)
}
return int(count), nil
}
func (r *RedisTracker) Get(ctx context.Context, userID string) (int, *time.Time, error) {
countKey := fmt.Sprintf("lockout:count:%s", userID)
lockKey := fmt.Sprintf("lockout:lock:%s", userID)
count, err := r.client.Get(ctx, countKey).Int()
if err == redis.Nil {
count = 0
} else if err != nil {
return 0, nil, err
}
ttl, err := r.client.TTL(ctx, lockKey).Result()
if err != nil || ttl <= 0 {
return count, nil, nil
}
until := time.Now().Add(ttl)
return count, &until, nil
}
func (r *RedisTracker) Lock(ctx context.Context, userID string, until time.Time) error {
key := fmt.Sprintf("lockout:lock:%s", userID)
return r.client.Set(ctx, key, "1", time.Until(until)).Err()
}
func (r *RedisTracker) Reset(ctx context.Context, userID string) error {
pipe := r.client.Pipeline()
pipe.Del(ctx, fmt.Sprintf("lockout:count:%s", userID))
pipe.Del(ctx, fmt.Sprintf("lockout:lock:%s", userID))
_, err := pipe.Exec(ctx)
return err
}Register the custom tracker:
import (
redislockout "myapp/lockout/redis"
"github.com/redis/go-redis/v9"
)
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
auth := authsome.New(
authsome.WithLockoutTracker(redislockout.New(rdb, time.Hour)),
authsome.WithLockout(lockout.Config{
MaxAttempts: 5,
LockoutDuration: 15 * time.Minute,
ResetAfter: 1 * time.Hour,
}),
)Admin operations
Administrators can manually unlock accounts and inspect lockout state:
// Check if a user is locked out.
count, lockedUntil, err := auth.Lockout().GetStatus(ctx, userID)
if lockedUntil != nil {
fmt.Printf("user %s locked until %s (after %d failures)\n",
userID, lockedUntil.Format(time.RFC3339), count)
}
// Manually unlock an account (e.g., after verifying identity via support).
err := auth.Lockout().Reset(ctx, userID)
// Manually lock an account.
until := time.Now().Add(24 * time.Hour)
err := auth.Lockout().Lock(ctx, userID, until)HTTP API endpoints
| Method | Path | Description |
|---|---|---|
GET | /admin/users/:user_id/lockout | Get the current lockout status for a user |
DELETE | /admin/users/:user_id/lockout | Manually unlock an account |
POST | /admin/users/:user_id/lockout | Manually lock an account |
These admin endpoints require the manage:user RBAC permission. Expose them only to internal admin tooling.
Events emitted
| Event | Trigger |
|---|---|
auth.signin.failed | A failed sign-in attempt was recorded (includes current count in metadata) |
auth.lockout.triggered | The account was locked after reaching MaxAttempts |
auth.lockout.lifted | The lockout expired or was manually cleared |
Role-Based Access Control
Hierarchical role and permission management with global and organization-scoped assignments, Warden integration, and RBAC middleware.
Rate Limiting
Per-endpoint rate limits with configurable windows, memory and noop built-in limiters, and a pluggable interface for Redis or custom backends.