lockout

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package lockout provides typed failed-credential windows and temporary lockout. It is distinct from request-rate limiting: concurrent admitted attempts can still complete. Pair it with ratelimit before expensive credential work.

Index

Constants

View Source
const KeyFeature keyspace.Feature = "lockout"

KeyFeature is the physical address family owned by this adapter.

View Source
const MaxDuration = ratelimit.MaxWindow
View Source
const MaxFailures uint32 = 10000

Variables

This section is empty.

Functions

func ValidateOperation

func ValidateOperation(ctx context.Context, key Key, policy Policy) error

Types

type Admission

type Admission struct {
	Decision Decision
	Snapshot Snapshot
}

Admission returns a snapshot only for an allowed attempt.

func (Admission) Validate

func (a Admission) Validate(p Policy) error

type Backend

type Backend interface {
	LockoutBegin(context.Context, Key, Policy, Generation) (Admission, error)
	LockoutFinish(context.Context, Key, Policy, Snapshot, Outcome) (Decision, error)
	LockoutReset(context.Context, Key, Policy) (bool, error)
}

Backend owns atomic transitions and the authority clock. Begin records the candidate generation only when creating a new window. Finish must use the exact admitted Snapshot once; it checks current lock/expiry even after successful credential verification. Success clears only when the revision still matches. Reset invalidates all outstanding snapshots for that key. Live policy changes, corrupt state and backward clocks fail closed. No operation retries uncertain mutations or falls back to another authority. The caller owns lifecycle.

Backend methods are trusted adapter boundaries, not public credential proof. An error always returns a zero result. External adapters must provide bounded state, atomic count+expiry updates and distinct generation values after reset.

type Code

type Code string
const (
	Locked      Code = "login_locked_out"
	Expired     Code = "login_attempt_expired"
	Unavailable Code = "login_protection_unavailable"
)

func (Code) Error

func (c Code) Error() string

type Config

type Config struct {
	Namespace                                   keyspace.Namespace
	MaxKeyBytes, MaxDeclarations, MaxConcurrent int
	Timeout                                     time.Duration
}

Config bounds all key encoding, backend calls and credential callbacks held by a borrowing store. Timeout must fit the intended verification work. A callback retaining control after cancellation also retains its concurrency slot.

func DefaultConfig

func DefaultConfig(namespace keyspace.Namespace) Config

func (Config) Validate

func (c Config) Validate() error

type Decision

type Decision struct {
	Status     Status
	RetryAfter time.Duration
	Triggered  bool
}

Decision describes the authority's state after an operation. RetryAfter is relative to that operation. Triggered is true only for the failure that starts a lock; it can drive an in-process notification, never a durable delivery claim.

func (Decision) Validate

func (d Decision) Validate(p Policy) error

type Declaration

type Declaration[K any] struct {
	// contains filtered or unexported fields
}

Declaration retains the submitted login key type. Include tenant/provider identity in its codec or family name, and use the same normalization as lookup. Missing and existing accounts must use the same policy and key construction.

func Define

func Define[K any](name Name, codec keyspace.Codec[K], policy Policy) Declaration[K]

Define declares a single-key throttle, for example MFA attempts keyed by an already authenticated subject. Submitted login identifiers use DefineLogin.

func DefineLogin

func DefineLogin[K any](name Name, codec keyspace.Codec[K], limits Limits) Declaration[K]

DefineLogin declares a client-aware throttle for submitted login identifiers. Failures count per (account, trusted client IP) with limits.PerClient, plus the Account and Address ceilings. The client IP comes from the request attribution established by the Foundry HTTP server (trusted proxy rules apply); without one, attempts share an "unknown" client and skip the Address ceiling. Policy reports limits.PerClient.

func (Declaration[K]) Bind

func (d Declaration[K]) Bind(store *Store) (Throttle[K], error)

func (Declaration[K]) Limits

func (d Declaration[K]) Limits() (Limits, bool)

Limits returns the client-aware limits of a DefineLogin declaration.

func (Declaration[K]) Name

func (d Declaration[K]) Name() Name

func (Declaration[K]) Policy

func (d Declaration[K]) Policy() Policy

func (Declaration[K]) Validate

func (d Declaration[K]) Validate() error

type Generation

type Generation struct {
	// contains filtered or unexported fields
}

Generation is a random window identity. It prevents an expired/reset window from being confused with a newly created window that has the same counters. This adapter token is not an authentication credential.

func NewGeneration

func NewGeneration() (Generation, error)

func ParseGeneration

func ParseGeneration(text string) (Generation, error)

func (Generation) GoString

func (g Generation) GoString() string

func (Generation) String

func (Generation) String() string

func (Generation) Token

func (g Generation) Token() string

func (Generation) Validate

func (g Generation) Validate() error

type Key

type Key struct {
	// contains filtered or unexported fields
}

Key is an adapter address, distinct from cache, lease and request-limit keys. Logical identifiers are hashed by the shared address codec, not logged raw.

func NewKey

func NewKey(namespace keyspace.Namespace, name Name, logical string) (Key, error)

func (Key) Namespace

func (k Key) Namespace() keyspace.Namespace

func (Key) String

func (k Key) String() string

func (Key) Validate

func (k Key) Validate() error

type Limits

type Limits struct {
	PerClient Policy
	Account   Policy
	Address   value.Optional[Policy]
}

Limits is the client-aware login throttle. PerClient counts failures for one account from one client address, so a single source cannot keep a victim locked out for everyone. Account is a higher per-account ceiling that still bounds distributed guessing; its tradeoff is that an attacker spreading failures across many addresses can lock one targeted account for LockFor.

Address is an optional per-client-address ceiling across all accounts that bounds credential spraying from one source. It is off by default: every user behind a shared address (carrier-grade NAT, a corporate proxy or VPN) shares it, so it can lock all of them out, including correct passwords, and with misconfigured trusted proxies every request appears to come from the proxy and one ceiling covers the whole site. Enable it (for example with DefaultAddressPolicy) only when client addresses are trusted and rarely shared, and prefer HTTP request rate limits for spraying. Each enabled ceiling must be at least the PerClient threshold.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits enables the pair and account windows; Address is off.

func (Limits) Validate

func (l Limits) Validate() error

type Name

type Name string

type Notice

type Notice[K any] struct {
	// contains filtered or unexported fields
}

Notice retains the declaration's concrete key for an explicit domain event or operational notification. Routine formatting/JSON hides account identifiers. This is an in-process observation after state commits, not durable delivery.

func (Notice[K]) Format

func (Notice[K]) Format(s fmt.State, _ rune)

func (Notice[K]) Key

func (n Notice[K]) Key() K

func (Notice[K]) RetryAfter

func (n Notice[K]) RetryAfter() time.Duration

type Outcome

type Outcome uint8
const (
	Failed Outcome = iota + 1
	Succeeded
)

func (Outcome) Validate

func (o Outcome) Validate() error

type Policy

type Policy struct {
	MaxFailures     uint32
	Window, LockFor time.Duration
}

Policy counts completed failures in a window starting at the first admitted attempt. Reaching MaxFailures locks until LockFor elapses. Denials never extend that expiry. A successful attempt clears only failures observed at its start; later failures are retained. Window expiry or an explicit Reset invalidates outstanding attempts, which must be retried rather than granting authority.

func DefaultAddressPolicy

func DefaultAddressPolicy() Policy

DefaultAddressPolicy is the documented opt-in per-address ceiling: 100 failures across all accounts in 15 minutes lock that address for 15 minutes.

func DefaultPolicy

func DefaultPolicy() Policy

func (Policy) Validate

func (p Policy) Validate() error

type Rejection

type Rejection struct {
	// contains filtered or unexported fields
}

Rejection carries a safe relative retry duration, never the submitted key. Runtime construction follows a validated backend decision.

func (*Rejection) Error

func (e *Rejection) Error() string

func (*Rejection) Is

func (e *Rejection) Is(target error) bool

func (*Rejection) RetryAfter

func (e *Rejection) RetryAfter() time.Duration

func (*Rejection) Triggered

func (e *Rejection) Triggered() bool

Triggered reports whether this attempt's failure started the lock, rather than being denied by an existing lock.

func (*Rejection) Unwrap

func (e *Rejection) Unwrap() error

type Snapshot

type Snapshot struct {
	Generation Generation
	Revision   uint32
}

Snapshot is an admission's immutable adapter observation. It is not a lease or permission to skip the final state check. Revision changes on completed failure and successful clearing, so a late success cannot erase newer failures.

func (Snapshot) Validate

func (s Snapshot) Validate() error

type Status

type Status uint8
const (
	StatusAllowed Status = iota + 1
	StatusLocked
	StatusExpired
)

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store borrows a backend without I/O or background work. One store may bind distinct password, MFA and recovery declarations; names isolate their policy.

func NewStore

func NewStore(backend Backend, config Config) (*Store, error)

func (*Store) Namespace

func (s *Store) Namespace() keyspace.Namespace

type Throttle

type Throttle[K any] struct {
	// contains filtered or unexported fields
}

Throttle owns one factor's failed-attempt policy, retaining the submitted key type. Its callbacks must not issue credentials: Run checks current lock state after verification before returning true. Already admitted attempts can overlap; use request-rate limits to bound that work independently.

func (Throttle[K]) Limits

func (t Throttle[K]) Limits() (Limits, bool)

Limits returns the client-aware limits when this throttle came from DefineLogin.

func (Throttle[K]) Name

func (t Throttle[K]) Name() Name

func (Throttle[K]) Policy

func (t Throttle[K]) Policy() Policy

func (Throttle[K]) Reset

func (t Throttle[K]) Reset(ctx context.Context, key K) (bool, error)

Reset is an explicit administrative/account-recovery operation. Authorize its caller. It invalidates outstanding attempts; ordinary login success uses the revision check in Run instead. It does not alter request-rate-limit quotas. A DefineLogin throttle clears the account ceiling and the pair for the current request's client; other clients' pair windows expire on their own.

func (Throttle[K]) Run

func (t Throttle[K]) Run(ctx context.Context, key K, verify func(context.Context) (bool, error)) (bool, error)

Run verifies one credential attempt. False,nil records a failure; true,nil conditionally clears old failures. An error or abnormal callback exit records neither and returns no authority. Backend uncertainty never falls back or retries, and a late lock/expiry prevents a successful callback from succeeding. A pending-MFA password proof verifies only that password stage; use a separate declaration for MFA attempts so success here cannot clear MFA failures.

func (Throttle[K]) Validate

func (t Throttle[K]) Validate() error

func (Throttle[K]) WithLockedObserver

func (t Throttle[K]) WithLockedObserver(observer func(context.Context, Notice[K]) error) (Throttle[K], error)

WithLockedObserver returns an immutable view. One confirmed threshold transition calls it once; already-locked denials do not. Lost backend replies can lose the notification. Errors cannot undo lockout and remain causes of its rejection.

Directories

Path Synopsis
Package memory supplies an explicit single-process lockout authority.
Package memory supplies an explicit single-process lockout authority.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL