Documentation
¶
Overview ¶
Package account defines the account lifecycle domain (signup, signin, verification, password reset).
Index ¶
- Constants
- Variables
- func CheckPassword(hash, password string) error
- func CheckPasswordArgon2(encoded, password string) error
- func GenerateOTPCode() (string, error)
- func GenerateVerificationToken() (string, error)
- func HashPassword(password string, cost int) (string, error)
- func HashPasswordArgon2(password string, params Argon2Params) (string, error)
- func HashPasswordWithPolicy(password string, policy PasswordPolicy) (string, error)
- func IsArgon2Hash(hash string) bool
- func NeedsRehash(hash string, policy PasswordPolicy) bool
- func NewSession(appID id.AppID, userID id.UserID, cfg SessionConfig) (*session.Session, error)
- func NewUser(req *SignUpRequest, passwordHash string) *user.User
- func RefreshSession(sess *session.Session, cfg SessionConfig) error
- type Argon2Params
- type BreachChecker
- type LockedError
- type MemoryPasswordHistoryStore
- type PasswordHistoryEntry
- type PasswordHistoryStore
- type PasswordPolicy
- type PasswordReset
- type SessionConfig
- type SignInRequest
- type SignUpRequest
- type Store
- type Verification
- type VerificationType
Constants ¶
const BreachCheckTimeout = 5 * time.Second
BreachCheckTimeout bounds one HIBP range lookup.
Variables ¶
var ( ErrInvalidCredentials = errors.New("account: invalid credentials") ErrEmailTaken = errors.New("account: email already taken") ErrUsernameTaken = errors.New("account: username already taken") ErrUserBanned = errors.New("account: user is banned") ErrAccountLocked = errors.New("account: account temporarily locked due to too many failed attempts") ErrSessionExpired = errors.New("account: session expired") ErrWeakPassword = errors.New("account: password does not meet requirements") ErrPasswordExpired = errors.New("account: password has expired and must be changed") ErrPasswordReused = errors.New("account: password was recently used and cannot be reused") ErrEmailNotVerified = errors.New("account: email address must be verified before signing in") ErrTooManyAttempts = errors.New("account: too many verification attempts; request a new code") ErrRateLimited = errors.New("account: too many attempts for this identifier; try again later") ErrMFARequired = errors.New("account: MFA challenge required to complete sign-in") )
Sentinel errors for account operations.
ErrBreachCheckUnavailable is what IsBreachedContext returns when the lookup could not run: the request failed, HIBP answered something other than 200, or the body could not be read. The password is not reported as breached in that case. The transport's own error is not wrapped, because it quotes the request URL and the URL carries the hash prefix.
var ErrPasswordBreached = fmt.Errorf("%w: password found in data breach", ErrWeakPassword)
ErrPasswordBreached is returned when a password has been found in a data breach.
Functions ¶
func CheckPassword ¶
CheckPassword verifies a password against a hash, auto-detecting the algorithm from the hash prefix. Supports bcrypt ($2a$/$2b$) and Argon2id ($argon2id$).
func CheckPasswordArgon2 ¶
CheckPasswordArgon2 verifies a password against an Argon2id PHC-format hash.
func GenerateOTPCode ¶
GenerateOTPCode returns a cryptographically random 6-digit numeric code, zero-padded (e.g. "004271"). Used for email/phone OTP verification where the user types a short code instead of clicking a long token link.
func GenerateVerificationToken ¶
GenerateVerificationToken generates a new verification token string.
func HashPassword ¶
HashPassword hashes a password using bcrypt (legacy convenience function).
func HashPasswordArgon2 ¶
func HashPasswordArgon2(password string, params Argon2Params) (string, error)
HashPasswordArgon2 hashes a password using Argon2id and returns a PHC-format string.
func HashPasswordWithPolicy ¶
func HashPasswordWithPolicy(password string, policy PasswordPolicy) (string, error)
HashPasswordWithPolicy hashes a password using the algorithm specified in the policy.
func IsArgon2Hash ¶
IsArgon2Hash returns true if the hash string is an Argon2id hash.
func NeedsRehash ¶
func NeedsRehash(hash string, policy PasswordPolicy) bool
NeedsRehash returns true if the hash should be regenerated using the current policy. It triggers in three cases:
- Algorithm changed (e.g. bcrypt → argon2id or vice versa).
- Argon2id hash uses parameters weaker than the policy currently demands (memory, iterations, or parallelism). Operators bumping these values to keep up with hardware progress get transparent rehash on next login without forcing a password reset.
- Bcrypt hash uses a cost lower than the policy's BcryptCost.
Returns false for unparseable hashes — better to leave a working hash alone than to fail-closed and force a reset on a corrupted row.
func NewSession ¶
NewSession creates a new session for a user.
func NewUser ¶
func NewUser(req *SignUpRequest, passwordHash string) *user.User
NewUser creates a new user from a signup request.
func RefreshSession ¶
func RefreshSession(sess *session.Session, cfg SessionConfig) error
RefreshSession generates new tokens for an existing session. When cfg.RotateRefreshToken is true, both the access token and refresh token are regenerated (the old refresh token becomes invalid). When false, only the access token is rotated and the refresh token is reused.
Types ¶
type Argon2Params ¶
type Argon2Params struct {
Memory uint32 // Memory in KiB (default: 64*1024 = 64 MiB)
Iterations uint32 // Time cost (default: 3)
Parallelism uint8 // Parallelism factor (default: 2)
SaltLength uint32 // Salt length in bytes (default: 16)
KeyLength uint32 // Derived key length in bytes (default: 32)
}
Argon2Params holds the parameters for Argon2id hashing.
func DefaultArgon2Params ¶
func DefaultArgon2Params() Argon2Params
DefaultArgon2Params returns OWASP-recommended Argon2id parameters.
type BreachChecker ¶
type BreachChecker struct {
// contains filtered or unexported fields
}
BreachChecker checks passwords against the HaveIBeenPwned Passwords API using the k-Anonymity model (only a 5-character SHA-1 prefix is sent).
func NewBreachChecker ¶
func NewBreachChecker() *BreachChecker
NewBreachChecker creates a new breach checker with sensible defaults.
func NewBreachCheckerWithClient ¶
func NewBreachCheckerWithClient(client *http.Client) *BreachChecker
NewBreachCheckerWithClient is NewBreachChecker sending its lookups through client. A nil client is a plain one with BreachCheckTimeout.
func (*BreachChecker) IsBreached ¶
func (bc *BreachChecker) IsBreached(password string) (bool, error)
IsBreached checks if a password appears in known data breaches. Returns true if the password hash appears in the HIBP database. On network errors, returns false (fail-open to avoid blocking auth).
func (*BreachChecker) IsBreachedContext ¶
IsBreachedContext is IsBreached with the lookup made under ctx, so it carries ctx's values (its trace span among them) and stops when ctx does. It fails open too, reporting false, but says so: when the lookup could not run the error is ErrBreachCheckUnavailable, so a caller can count a control that did not run.
type LockedError ¶
LockedError is ErrAccountLocked carrying when the lock lifts, so a handler can answer 423 with a Retry-After. errors.Is(err, ErrAccountLocked) holds for it.
func (*LockedError) Error ¶
func (e *LockedError) Error() string
func (*LockedError) Is ¶
func (e *LockedError) Is(target error) bool
Is lets errors.Is match the sentinel.
func (*LockedError) RetryAfter ¶
func (e *LockedError) RetryAfter(now time.Time) int
RetryAfter is the whole seconds until the lock lifts, at least one.
type MemoryPasswordHistoryStore ¶
type MemoryPasswordHistoryStore struct {
// contains filtered or unexported fields
}
MemoryPasswordHistoryStore is an in-memory password history store for development and testing.
func NewMemoryPasswordHistoryStore ¶
func NewMemoryPasswordHistoryStore() *MemoryPasswordHistoryStore
NewMemoryPasswordHistoryStore creates an in-memory password history store.
func (*MemoryPasswordHistoryStore) GetPasswordHistory ¶
func (s *MemoryPasswordHistoryStore) GetPasswordHistory(_ context.Context, userID id.UserID, limit int) ([]PasswordHistoryEntry, error)
func (*MemoryPasswordHistoryStore) SavePasswordHash ¶
type PasswordHistoryEntry ¶
type PasswordHistoryEntry struct {
ID string `json:"id"`
UserID id.UserID `json:"user_id"`
Hash string `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
PasswordHistoryEntry records a previously used password hash.
type PasswordHistoryStore ¶
type PasswordHistoryStore interface {
// SavePasswordHash records a new password hash for the user.
SavePasswordHash(ctx context.Context, userID id.UserID, hash string) error
// GetPasswordHistory returns the most recent N password hashes.
GetPasswordHistory(ctx context.Context, userID id.UserID, limit int) ([]PasswordHistoryEntry, error)
}
PasswordHistoryStore persists previous password hashes so they cannot be reused. Implementations should store only the hash, never plaintext.
type PasswordPolicy ¶
type PasswordPolicy struct {
MinLength int
RequireUppercase bool
RequireLowercase bool
RequireDigit bool
RequireSpecial bool
BcryptCost int
Algorithm string // "bcrypt" (default) or "argon2id"
Argon2Params Argon2Params // Used when Algorithm = "argon2id"
CheckBreached bool // Enable HIBP breach checking
}
PasswordPolicy configures password validation rules.
func (PasswordPolicy) ValidatePassword ¶
func (p PasswordPolicy) ValidatePassword(password string) error
ValidatePassword checks a password against the policy.
type PasswordReset ¶
type PasswordReset struct {
ID id.PasswordResetID `json:"id"`
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
UserID id.UserID `json:"user_id"`
Token string `json:"-"`
TokenHash string `json:"-"`
ExpiresAt time.Time `json:"expires_at"`
Consumed bool `json:"consumed"`
CreatedAt time.Time `json:"created_at"`
}
PasswordReset represents a password reset token.
type SessionConfig ¶
type SessionConfig struct {
TokenTTL time.Duration
RefreshTokenTTL time.Duration
MaxActiveSessions int
RotateRefreshToken bool
// TokenExchangeTTL caps tokens minted by the RFC 8693 token exchange
// grant. Short by design: an exchanged token is meant to be re-minted,
// not held. Zero means the caller's own default applies.
TokenExchangeTTL time.Duration
}
SessionConfig holds session token configuration.
type SignInRequest ¶
type SignInRequest struct {
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
Email string `json:"email,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
// DPoPJKT binds the issued session to a client-held key (RFC 9449).
// Empty issues an ordinary unbound session.
DPoPJKT string `json:"-"`
}
SignInRequest is the input for account authentication.
type SignUpRequest ¶
type SignUpRequest struct {
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
Email string `json:"email"`
Password string `json:"password"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Username string `json:"username,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"` // custom form fields
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
// DPoPJKT binds the newly created session to a client-held key
// (RFC 9449). Empty issues an ordinary unbound session.
DPoPJKT string `json:"-"`
}
SignUpRequest is the input for account registration.
type Store ¶
type Store interface {
CreateVerification(ctx context.Context, v *Verification) error
GetVerification(ctx context.Context, token string) (*Verification, error)
ConsumeVerification(ctx context.Context, token string) error
// GetActiveEmailVerification returns the most recent unconsumed, unexpired
// email verification for the user, or ErrNotFound. The OTP flow looks up by
// user (codes are short and not globally unique) rather than by token.
GetActiveEmailVerification(ctx context.Context, userID id.UserID) (*Verification, error)
// UpdateVerification persists mutable fields of an existing verification
// (Attempts, Consumed).
UpdateVerification(ctx context.Context, v *Verification) error
CreatePasswordReset(ctx context.Context, pr *PasswordReset) error
GetPasswordReset(ctx context.Context, token string) (*PasswordReset, error)
ConsumePasswordReset(ctx context.Context, token string) error
}
Store defines the persistence interface for account lifecycle operations.
type Verification ¶
type Verification struct {
ID id.VerificationID `json:"id"`
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
UserID id.UserID `json:"user_id"`
// Token is the plaintext, present on the way into the store and on a
// verification looked up by that plaintext. TokenHash is what the store
// keeps; a verification read back by user carries only the hash.
Token string `json:"-"`
TokenHash string `json:"-"`
Type VerificationType `json:"type"`
Attempts int `json:"attempts"`
ExpiresAt time.Time `json:"expires_at"`
Consumed bool `json:"consumed"`
CreatedAt time.Time `json:"created_at"`
}
Verification represents an email or phone verification token.
Token holds either a long random token (link-based flows like magic link) or a short 6-digit OTP code (email verification). For OTP flows, Attempts tracks failed entry attempts so callers can enforce a maximum before requiring a resend.
func NewEmailVerificationCode ¶
func NewEmailVerificationCode(appID id.AppID, userID id.UserID, ttl time.Duration) (*Verification, error)
NewEmailVerificationCode creates an email-verification record carrying a 6-digit OTP code (stored in Token) that the user types to verify. Attempts starts at 0.
func NewVerification ¶
func NewVerification(_ context.Context, appID id.AppID, userID id.UserID, vType VerificationType, ttl time.Duration) (*Verification, error)
NewVerification creates a new email/phone verification record.
type VerificationType ¶
type VerificationType string
VerificationType identifies the kind of verification.
const ( VerificationEmail VerificationType = "email" VerificationPhone VerificationType = "phone" )