Documentation
¶
Overview ¶
Package ulinzi is a Go port of the .NET 8 security library (identity, opaque sessions, MFA, tamper-evident audit, password/lockout policy, RBAC permissions), all isolated in a dedicated PostgreSQL "security" schema.
The importable packages are a pure abstraction of functions and types and embed no SQL: the "security" schema is owned by the optional migrate subpackage (github.com/Cyrus-0101/ulinzilib-go/migrate), applied via cmd/ulinzi-migrate.
Capabilities:
- Session validation + global logout: Service.ValidateSession, Service.SessionEnforcement, Service.InvalidateUserSessions.
- Login / issuance: Service.Login, Service.Logout, Service.CreateUser, Service.ChangePassword (enabled with WithAuthStore).
- Pluggable MFA registry: MfaMethod — TOTPService + RecoveryCodeMethod (wired with WithMfaMethods), with secret encryption + replay guard.
- Password policy (WithPasswordPolicy), brute-force lockout (WithLockout), and tamper-evident audit (WithAudit).
Roadmap surfaces (WebAuthn, OIDC, trusted devices, IP whitelisting) are stable interfaces in stubs.go returning ErrNotImplemented.
Index ¶
- Constants
- Variables
- func ContextWithPrincipal(ctx context.Context, p *Principal) context.Context
- func ValidatePassword(ctx context.Context, password string, policy PasswordPolicy, hibp HibpService) []string
- type AESGCMSecretProtector
- type ActiveLockout
- type AuditEvent
- type AuditService
- type AuditStore
- type AuthStore
- type Config
- type DefaultHibpService
- type EnrollResult
- type HibpService
- type IPWhitelistService
- type IdentityProvider
- type LockoutPolicy
- type LockoutService
- func (s *LockoutService) CheckLockout(ctx context.Context, email string) (locked bool, info *ActiveLockout, err error)
- func (s *LockoutService) RecordFailure(ctx context.Context, email string, userID *string, reason, ip, ua string) (bool, error)
- func (s *LockoutService) RecordSuccess(ctx context.Context, email string, userID *string, ip, ua string) error
- func (s *LockoutService) Unlock(ctx context.Context, email, reason string, byUserID *string) error
- type LockoutStore
- type LoginAttempt
- type LoginInput
- type LoginResult
- type Mfa
- type MfaMethod
- type MfaSecretProtector
- type MfaStore
- type MfaUsageStore
- type NewUser
- type NoopHibpService
- type NoopSecretProtector
- type OIDCService
- type Option
- func WithAudit(store AuditStore) Option
- func WithAuthStore(a AuthStore) Option
- func WithHibp(h HibpService) Option
- func WithLockout(store LockoutStore) Option
- func WithMfaMethods(methods ...MfaMethod) Option
- func WithPasswordPolicy(store PasswordPolicyStore) Option
- func WithSecurityStamps(store SecurityStampStore) Option
- type PasswordPolicy
- type PasswordPolicyError
- type PasswordPolicyStore
- type PasswordStore
- type PermissionError
- type Principal
- type RecoveryCodeMethod
- type SecurityStampStore
- type Service
- func (s *Service) Audit() *AuditService
- func (s *Service) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error
- func (s *Service) CookieName() string
- func (s *Service) CreateUser(ctx context.Context, in NewUser) (uuid.UUID, error)
- func (s *Service) HasPermission(ctx context.Context, permissionKey string) bool
- func (s *Service) InvalidateUserSessions(ctx context.Context, userID uuid.UUID) error
- func (s *Service) Login(ctx context.Context, in LoginInput) (*LoginResult, error)
- func (s *Service) Logout(ctx context.Context, sessionID string) error
- func (s *Service) RequireContextPermission(ctx context.Context, permissionKey string) error
- func (s *Service) RequirePermission(key string, next http.Handler) http.Handler
- func (s *Service) SessionEnforcement(next http.Handler) http.Handler
- func (s *Service) UnlockAccount(ctx context.Context, email, reason string, byUserID *string) error
- func (s *Service) ValidateSession(ctx context.Context, sessionID string) (*Principal, error)
- type SessionCapStore
- type SessionPolicy
- type SessionRecord
- type Store
- type TOTPOption
- type TOTPService
- func (s *TOTPService) Disable(ctx context.Context, userID uuid.UUID) error
- func (s *TOTPService) EnrollTOTP(ctx context.Context, userID uuid.UUID) (EnrollResult, error)
- func (s *TOTPService) IsEnabled(ctx context.Context, userID uuid.UUID) (bool, error)
- func (s *TOTPService) IsEnrolled(ctx context.Context, userID uuid.UUID) (bool, error)
- func (s *TOTPService) Name() string
- func (s *TOTPService) Verify(ctx context.Context, userID uuid.UUID, code string) (bool, error)
- func (s *TOTPService) VerifyRecoveryCode(ctx context.Context, userID uuid.UUID, code string) (bool, error)
- func (s *TOTPService) VerifyTOTP(ctx context.Context, userID uuid.UUID, code string) (bool, error)
- type TrustedDeviceService
- type UnimplementedIPWhitelistService
- type UnimplementedOIDCService
- type UnimplementedTrustedDeviceService
- type UnimplementedWebAuthnService
- func (UnimplementedWebAuthnService) BeginLogin(context.Context, string) ([]byte, error)
- func (UnimplementedWebAuthnService) BeginRegistration(context.Context, string) ([]byte, error)
- func (UnimplementedWebAuthnService) FinishLogin(context.Context, string, []byte) error
- func (UnimplementedWebAuthnService) FinishRegistration(context.Context, string, []byte) error
- type User
- type UserAuth
- type WebAuthnService
Constants ¶
const ( AuditUserCreated = "UserCreated" AuditPasswordChanged = "PasswordChanged" AuditPasswordReset = "PasswordReset" AuditPasswordResetRequested = "PasswordResetRequested" AuditEmailConfirmed = "EmailConfirmed" AuditEmailChanged = "EmailChanged" AuditLoginSucceeded = "PasswordSignIn" AuditLoginFailed = "PasswordSignInFailed" AuditLogout = "Logout" AuditMfaEnrolled = "MfaEnrolled" AuditMfaVerified = "MfaVerified" AuditMfaDisabled = "MfaDisabled" AuditAccountLocked = "AccountLocked" AuditSessionsInvalidated = "SessionsInvalidated" )
Audit event types. UserCreated..SessionsInvalidated match the .NET string literals; the MFA/logout/lockout events are additions (the .NET reference does not audit those).
const ( AuditStatusSuccess = "success" AuditStatusFailure = "failure" )
AuditStatusSuccess / AuditStatusFailure are the two outcome values.
const Version = "0.3.0"
Version is the current module version.
Variables ¶
var ( // ErrInvalidCredentials is returned for any bad email/password/MFA combo // (deliberately uniform — never leak which factor failed). ErrInvalidCredentials = errors.New("ulinzi: invalid credentials") // ErrMFARequired is returned when a user has MFA enabled but Login was // called without a TOTP/recovery code. The caller should prompt for it and // retry with LoginInput.TOTPCode set. ErrMFARequired = errors.New("ulinzi: mfa code required") // ErrEmailTaken is returned by CreateUser when the email already exists. ErrEmailTaken = errors.New("ulinzi: email already registered") // when the Service was built without the required store (WithAuthStore). ErrAuthUnavailable = errors.New("ulinzi: auth store not configured (use New(store, cfg, WithAuthStore(...)))") )
Login / provisioning errors.
var ( // ErrSessionNotFound is returned when no session matches the opaque cookie. ErrSessionNotFound = errors.New("ulinzi: session not found") // ErrSessionRevoked is returned when the session has been logged out. ErrSessionRevoked = errors.New("ulinzi: session revoked") // ErrSessionExpired is returned on absolute or idle timeout. ErrSessionExpired = errors.New("ulinzi: session expired") // ErrUserInactive is returned when the session's user is disabled. ErrUserInactive = errors.New("ulinzi: user inactive") // ErrSessionStampChanged is returned when the user's security stamp changed // after the session was issued (password/MFA/role change, or forced global // logout). Callers should treat it like ErrSessionRevoked and re-authenticate. ErrSessionStampChanged = errors.New("ulinzi: session invalidated (security stamp changed)") // ErrAccountLocked is returned by Login when the account is locked out by the // brute-force lockout policy. ErrAccountLocked = errors.New("ulinzi: account locked out") // Service was built without WithLockout. ErrLockoutUnavailable = errors.New("ulinzi: lockout not configured (use WithLockout)") // ErrNoPrincipal is returned when a context carries no validated principal. ErrNoPrincipal = errors.New("ulinzi: no principal in context") // ErrNotImplemented marks wave-2 surface that is not ported yet. ErrNotImplemented = errors.New("ulinzi: not implemented (wave-2)") )
var ErrMfaNotEnrolled = errors.New("ulinzi: mfa not enrolled")
ErrMfaNotEnrolled is returned when an MFA operation is attempted for a user who has no enrolment row.
var ErrSecretProtection = errors.New("ulinzi: mfa secret could not be decrypted")
ErrSecretProtection is returned when a stored secret cannot be decrypted and is not recognizable legacy plaintext (fail closed).
Functions ¶
func ContextWithPrincipal ¶
ContextWithPrincipal returns a copy of ctx carrying the principal.
func ValidatePassword ¶
func ValidatePassword(ctx context.Context, password string, policy PasswordPolicy, hibp HibpService) []string
ValidatePassword returns all policy violations for password; an empty slice means valid. An inactive policy always passes. The breach-database (HIBP) check runs LAST and only when there are no structural violations. Character counting is by rune (Unicode-aware); "special" is any non-letter, non-digit.
Types ¶
type AESGCMSecretProtector ¶
type AESGCMSecretProtector struct {
// contains filtered or unexported fields
}
AESGCMSecretProtector implements MfaSecretProtector with AES-256-GCM. The stored form is "v1:" + base64std(nonce || ciphertext || tag).
func NewAESGCMSecretProtector ¶
func NewAESGCMSecretProtector(key []byte) (*AESGCMSecretProtector, error)
NewAESGCMSecretProtector builds a protector from a 16/24/32-byte key (32 = AES-256, recommended). Source the key from a KMS/secret manager, not source.
func (*AESGCMSecretProtector) Protect ¶
func (p *AESGCMSecretProtector) Protect(secret string) (string, error)
func (*AESGCMSecretProtector) Unprotect ¶
func (p *AESGCMSecretProtector) Unprotect(ciphertext string) (string, error)
func (*AESGCMSecretProtector) UnprotectOrLegacy ¶
func (p *AESGCMSecretProtector) UnprotectOrLegacy(value string) (string, bool, error)
type ActiveLockout ¶
ActiveLockout is the most recent open (not admin-unlocked) lockout for an email.
func (*ActiveLockout) IsPermanent ¶
func (a *ActiveLockout) IsPermanent() bool
IsPermanent reports whether the lockout never expires on its own.
type AuditEvent ¶
type AuditEvent struct {
EventType string // required
UserID string // subject/actor (GUID string)
SessionID string // opaque session id, if applicable
IPAddress string // source IP, if known
UserAgent string // client UA, if known
Details string // free-text detail (e.g. failure sub-reason)
Status string // "success" (default) or "failure"
OccurredAt time.Time // zero => now (UTC)
}
AuditEvent is a single security event to append to the log.
type AuditService ¶
type AuditService struct {
// contains filtered or unexported fields
}
AuditService writes a tamper-evident, hash-chained security audit log. Each row's hash covers its canonical fields plus the previous row's hash, so any edit or deletion of a historical row breaks the chain and is detectable by VerifyChain. (This is stronger than the .NET reference, which is a plain append log — ulinzilib-go implements the "tamper-evident" property for real.)
func NewAuditService ¶
func NewAuditService(store AuditStore) *AuditService
NewAuditService builds an AuditService over the given store.
func (*AuditService) Record ¶
func (s *AuditService) Record(ctx context.Context, e AuditEvent) error
Record appends one event. EventType is required; Status defaults to "success"; OccurredAt defaults to now. EventType/Status are truncated to their column widths.
func (*AuditService) VerifyChain ¶
VerifyChain recomputes the whole chain and reports the first tampered row (by seq), if any. ok=true means the chain is intact.
type AuditStore ¶
type AuditStore interface {
// AppendChained serializes appends (an advisory lock in the implementation),
// reads the previous row's hash, calls hashFn(prevHash) to compute this row's
// hash, and inserts the row with prev_hash + hash atomically.
AppendChained(ctx context.Context, e AuditEvent, hashFn func(prevHash string) string) error
// VerifyAuditChain walks the log in seq order and recomputes each row's hash
// with hashFn(prevHash, event); it returns ok=false and the seq of the first
// row whose stored hash or prev-linkage does not match.
VerifyAuditChain(ctx context.Context, hashFn func(prevHash string, e AuditEvent) string) (ok bool, brokenSeq int64, err error)
}
AuditStore persists chained audit rows.
type AuthStore ¶
type AuthStore interface {
GetUserAuthByEmail(ctx context.Context, normalizedEmail string) (*UserAuth, error) // nil, nil if none
CreateUser(ctx context.Context, id uuid.UUID, email, normalizedEmail, passwordHash, securityStamp string) error
CreateSession(ctx context.Context, rec *SessionRecord, loginCompletedAt time.Time) error
RevokeSession(ctx context.Context, sessionID, reason string, at time.Time) error
}
AuthStore is the write-side data dependency for login/session issuance and user provisioning. The PostgreSQL Store in ./postgres satisfies it alongside Store, MfaStore, and the optional PasswordStore / SessionCapStore.
type Config ¶
type Config struct {
// CookieName is the opaque session cookie the SessionEnforcement middleware
// reads. Defaults to "ulinzi.session".
CookieName string
// LastActivityThrottle bounds how often a valid session's last_activity_at
// is written back, mirroring UlinziLib's ~30s throttle to avoid write storms.
LastActivityThrottle time.Duration
// DefaultSessionPolicy is used when no row exists in security.session_policies.
DefaultSessionPolicy SessionPolicy
// PasswordHashCost is the bcrypt cost used by CreateUser; 0 uses bcrypt.DefaultCost.
PasswordHashCost int
}
Config tunes Service behaviour. The zero value is unusable; prefer DefaultConfig and override fields as needed. New also fills sensible defaults.
func DefaultConfig ¶
func DefaultConfig() Config
DefaultConfig returns production-sensible defaults.
type DefaultHibpService ¶
type DefaultHibpService struct {
// contains filtered or unexported fields
}
DefaultHibpService queries Have I Been Pwned's k-anonymity range API: only the first 5 hex chars of the password's SHA-1 are ever sent. It fails open.
func NewHibpService ¶
func NewHibpService() *DefaultHibpService
NewHibpService returns a HIBP client with a 5s timeout.
func (*DefaultHibpService) IsCompromised ¶
type EnrollResult ¶
type EnrollResult struct {
Secret string
ProvisioningURI string // otpauth:// URL for QR codes
RecoveryCodes []string
}
EnrollResult is returned once, at enrolment. Secret and RecoveryCodes are shown to the user now and never recoverable again (only protected/hashed forms persist).
type HibpService ¶
HibpService reports whether a password appears in a public breach corpus. Implementations MUST fail OPEN: any error returns (false, nil) so a breach lookup outage never blocks a password change. It is defense-in-depth only and is consulted by ValidatePassword solely when the policy enables it.
type IPWhitelistService ¶
IPWhitelistService gates access by client IP / CIDR.
type IdentityProvider ¶
type IdentityProvider string
IdentityProvider enumerates the authentication providers UlinziLib supports.
const ( ProviderLocal IdentityProvider = "Local" ProviderZitadel IdentityProvider = "Zitadel" ProviderEntra IdentityProvider = "Entra" ProviderGoogle IdentityProvider = "Google" )
type LockoutPolicy ¶
type LockoutPolicy struct {
IsActive bool
MaxFailedAttempts int // failures within the window that trigger a lockout
LockoutDuration time.Duration // base lockout duration
ObservationWindow time.Duration // sliding window for counting failures
ProgressiveEnabled bool // exponential escalation on repeat lockouts
ProgressiveMultiplier float64 // escalation base (e.g. 2.0)
MaxLockoutDuration time.Duration // cap on escalated duration
PermanentThreshold *int // lockout_count >= this => permanent; nil => never permanent
}
LockoutPolicy mirrors the enforced subset of security.lockout_policies.
func DefaultLockoutPolicy ¶
func DefaultLockoutPolicy() LockoutPolicy
DefaultLockoutPolicy mirrors the .NET defaults: 5 failures / 15m window, progressive x2 capped at 24h, never permanent.
type LockoutService ¶
type LockoutService struct {
// contains filtered or unexported fields
}
LockoutService applies progressive brute-force lockout keyed by (normalized) email. Call CheckLockout BEFORE verifying the password; call RecordFailure / RecordSuccess AFTER.
func NewLockoutService ¶
func NewLockoutService(store LockoutStore) *LockoutService
NewLockoutService builds a LockoutService over the given store.
func (*LockoutService) CheckLockout ¶
func (s *LockoutService) CheckLockout(ctx context.Context, email string) (locked bool, info *ActiveLockout, err error)
CheckLockout reports whether the email is currently locked out.
func (*LockoutService) RecordFailure ¶
func (s *LockoutService) RecordFailure(ctx context.Context, email string, userID *string, reason, ip, ua string) (bool, error)
RecordFailure appends a failed attempt and, if the threshold is reached within the observation window, opens a (possibly escalated/permanent) lockout. It returns whether the account is now locked.
func (*LockoutService) RecordSuccess ¶
func (s *LockoutService) RecordSuccess(ctx context.Context, email string, userID *string, ip, ua string) error
RecordSuccess appends a successful attempt. It does not close existing lockouts (escalation persists until admin unlock); the sliding window ages out naturally.
type LockoutStore ¶
type LockoutStore interface {
GetLockoutPolicy(ctx context.Context) (*LockoutPolicy, error) // nil if none
RecordLoginAttempt(ctx context.Context, a LoginAttempt) error
CountRecentFailures(ctx context.Context, email string, since time.Time) (int, error)
// GetActiveLockout returns the most recent lockout row not yet admin-unlocked
// (even if expired), or nil.
GetActiveLockout(ctx context.Context, email string) (*ActiveLockout, error)
InsertLockout(ctx context.Context, email string, userID *string, lockoutCount, failedCount int, lockedAt time.Time, expiresAt *time.Time) error
// UnlockAccount closes all open lockouts for the email (admin action). This is
// the only path that resets progressive escalation.
UnlockAccount(ctx context.Context, email, reason string, byUserID *string, at time.Time) error
}
LockoutStore is the data dependency for brute-force lockout, keyed by email.
type LoginAttempt ¶
type LoginAttempt struct {
Email string
UserID *string
Success bool
FailureReason string
IPAddress string
UserAgent string
AttemptType string // default "password"
IdentityProvider string // default "local"
}
LoginAttempt is one row appended to security.login_attempts.
type LoginInput ¶
type LoginInput struct {
Email string
Password string
TOTPCode string // required only when the user has MFA enabled
IPAddress string
UserAgent string
}
LoginInput carries credentials plus optional MFA + request metadata.
type LoginResult ¶
type LoginResult struct {
SessionID string // opaque token — set this as the session cookie value
Principal *Principal // the authenticated principal (roles + permissions)
}
LoginResult is the outcome of a successful Login.
type Mfa ¶
type Mfa struct {
ExternalUserID uuid.UUID
TwoFactorEnabled bool
AuthenticatorKey string // protected (encrypted) TOTP secret; see MfaSecretProtector
RecoveryCodeHashes []string // salted PBKDF2 hashes of the still-unused recovery codes
UpdatedAt time.Time
}
Mfa mirrors security.app_user_mfa.
type MfaMethod ¶
type MfaMethod interface {
// Name identifies the method ("totp", "recovery", …).
Name() string
// IsEnrolled reports whether the user has this factor set up.
IsEnrolled(ctx context.Context, userID uuid.UUID) (bool, error)
// Verify checks a submitted code/assertion, consuming it if single-use.
Verify(ctx context.Context, userID uuid.UUID, code string) (bool, error)
}
MfaMethod is one pluggable second factor. The Service holds an ordered set of methods (registered with WithMfaMethods); at login it treats a user as MFA-protected if ANY method reports IsEnrolled, and accepts the login if ANY enrolled method verifies the supplied code. New factors (WebAuthn, SMS, …) implement this interface without touching the login flow.
type MfaSecretProtector ¶
type MfaSecretProtector interface {
// Protect encrypts a Base32 TOTP secret for storage.
Protect(base32Secret string) (string, error)
// Unprotect decrypts stored ciphertext back to the Base32 secret. It errors
// on any value that is not ciphertext produced by Protect.
Unprotect(ciphertext string) (string, error)
// UnprotectOrLegacy decrypts ciphertext; on failure it returns the input as
// legacy plaintext ONLY if it matches the Base32 shape (^[A-Z2-7]+=*$),
// otherwise it fails closed. wasEncrypted reports whether the value was real
// ciphertext, so callers can re-encrypt legacy plaintext on next verify.
// Empty input returns ("", false, nil).
UnprotectOrLegacy(value string) (secret string, wasEncrypted bool, err error)
}
MfaSecretProtector encrypts TOTP shared secrets (security.app_user_mfa. authenticator_key) at rest. The secret is the entire second factor, so it must never be persisted in plaintext by new code. This mirrors the .NET IMfaSecretProtector (which wraps ASP.NET DataProtection); ulinzilib-go uses AES-256-GCM with a key supplied by the host.
type MfaStore ¶
type MfaStore interface {
GetMfa(ctx context.Context, userID uuid.UUID) (*Mfa, error) // nil, nil if none
UpsertMfa(ctx context.Context, m *Mfa) error
}
MfaStore persists MFA enrolment state (security.app_user_mfa).
type MfaUsageStore ¶
type MfaUsageStore interface {
// TryClaimMfaCode atomically records a one-time use of codeHash for userID
// with the given TTL. It returns true on first use, false if the code was
// already claimed within its window (a replay). On any uncertainty it must
// fail closed (return false).
TryClaimMfaCode(ctx context.Context, userID uuid.UUID, codeHash string, ttl time.Duration) (bool, error)
}
MfaUsageStore is the optional TOTP replay ledger (security.mfa_code_usages).
type NoopHibpService ¶
type NoopHibpService struct{}
NoopHibpService always reports not-compromised; the default when breach checking is disabled.
func (NoopHibpService) IsCompromised ¶
type NoopSecretProtector ¶
type NoopSecretProtector struct{}
NoopSecretProtector stores secrets verbatim (no encryption). It is the default when no protector is configured and is intended for tests/dev ONLY — it leaves TOTP seeds readable in the database. Configure an AESGCMSecretProtector in prod.
func (NoopSecretProtector) Protect ¶
func (NoopSecretProtector) Protect(secret string) (string, error)
func (NoopSecretProtector) Unprotect ¶
func (NoopSecretProtector) Unprotect(ciphertext string) (string, error)
func (NoopSecretProtector) UnprotectOrLegacy ¶
func (NoopSecretProtector) UnprotectOrLegacy(value string) (string, bool, error)
type OIDCService ¶
type OIDCService interface {
Exchange(ctx context.Context, provider IdentityProvider, code, redirectURI string) (userID string, err error)
}
OIDCService exchanges an external identity-provider assertion for a local user (multi-provider SSO — Zitadel / Entra / Google). Local password login is implemented today; federated login is on the roadmap.
type Option ¶
type Option func(*Service)
Option configures optional Service capabilities.
func WithAudit ¶
func WithAudit(store AuditStore) Option
WithAudit enables the tamper-evident (hash-chained) audit trail.
func WithAuthStore ¶
WithAuthStore enables the login/session-issuance and user-provisioning API (Login, Logout, CreateUser, ChangePassword). The PostgreSQL Store satisfies AuthStore.
func WithHibp ¶
func WithHibp(h HibpService) Option
WithHibp sets the breached-password checker consulted when the password policy has CheckBreachDatabase enabled.
func WithLockout ¶
func WithLockout(store LockoutStore) Option
WithLockout enables brute-force lockout + login-attempt tracking around Login.
func WithMfaMethods ¶
WithMfaMethods registers pluggable MFA factors (e.g. a TOTPService and a RecoveryCodeMethod). At login a user is treated as MFA-protected if any registered method reports IsEnrolled, and the login succeeds if any enrolled method verifies the submitted code.
func WithPasswordPolicy ¶
func WithPasswordPolicy(store PasswordPolicyStore) Option
WithPasswordPolicy enables password-policy enforcement in CreateUser and ChangePassword, reading the active policy from the store.
func WithSecurityStamps ¶
func WithSecurityStamps(store SecurityStampStore) Option
WithSecurityStamps enables security-stamp rotation, which forces global logout of a user's sessions on password/MFA/role change or explicit invalidation.
type PasswordPolicy ¶
type PasswordPolicy struct {
IsActive bool
MinLength int
MaxLength int
RequireUppercase bool
RequireLowercase bool
RequireDigit bool
RequireSpecial bool
MinUniqueChars int
CheckBreachDatabase bool
}
PasswordPolicy mirrors the enforced subset of security.password_policies. The stored-but-unenforced fields in the .NET reference (history, min/max age, allow-username) are intentionally omitted — like the .NET library, they are configuration the validator does not act on.
func DefaultPasswordPolicy ¶
func DefaultPasswordPolicy() PasswordPolicy
DefaultPasswordPolicy mirrors the .NET application defaults: 12..128 length, all character classes, 4 unique characters, breach check off.
type PasswordPolicyError ¶
type PasswordPolicyError struct{ Violations []string }
PasswordPolicyError aggregates every policy violation (validation does not fail fast, except that the breach check runs only when nothing else failed).
func (*PasswordPolicyError) Error ¶
func (e *PasswordPolicyError) Error() string
type PasswordPolicyStore ¶
type PasswordPolicyStore interface {
// GetPasswordPolicy returns the active policy, or nil if none is configured
// (callers fall back to DefaultPasswordPolicy).
GetPasswordPolicy(ctx context.Context) (*PasswordPolicy, error)
}
PasswordPolicyStore loads the active single-row password policy.
type PasswordStore ¶
type PasswordStore interface {
GetUserAuthByID(ctx context.Context, id uuid.UUID) (*UserAuth, error)
UpdatePasswordHash(ctx context.Context, id uuid.UUID, passwordHash string) error
}
PasswordStore is the optional dependency ChangePassword needs (satisfied by the PostgreSQL Store). Without it, ChangePassword returns ErrAuthUnavailable.
type PermissionError ¶
type PermissionError struct{ Key string }
PermissionError indicates the principal lacks a required permission key.
func (*PermissionError) Error ¶
func (e *PermissionError) Error() string
type Principal ¶
type Principal struct {
UserID uuid.UUID
SessionID string
Email string
Roles []string
// contains filtered or unexported fields
}
Principal is the validated identity attached to a request context after the SessionEnforcement middleware accepts the opaque session cookie.
func PrincipalFromContext ¶
PrincipalFromContext extracts the validated principal injected by SessionEnforcement, if any.
func (*Principal) Has ¶
Has reports whether the principal holds the given permission key (e.g. "users.canManage"). This is the Go analogue of the .NET IAuthorizationService.HasPermissionAsync(userId, key).
func (*Principal) Permissions ¶
Permissions returns the resolved permission keys (unordered copy).
type RecoveryCodeMethod ¶
type RecoveryCodeMethod struct {
// contains filtered or unexported fields
}
RecoveryCodeMethod is the single-use recovery-code factor. It shares the app_user_mfa row minted by TOTPService.EnrollTOTP and implements MfaMethod (Name "recovery"), so recovery codes are a first-class pluggable factor.
func NewRecoveryCodeMethod ¶
func NewRecoveryCodeMethod(store MfaStore) *RecoveryCodeMethod
NewRecoveryCodeMethod builds a recovery-code factor over the MFA store.
func (*RecoveryCodeMethod) IsEnrolled ¶
IsEnrolled implements MfaMethod: MFA is enabled and unused codes remain.
func (*RecoveryCodeMethod) Name ¶
func (m *RecoveryCodeMethod) Name() string
Name implements MfaMethod.
type SecurityStampStore ¶
type SecurityStampStore interface {
// BumpSecurityStamp sets security.users.security_stamp to newStamp.
BumpSecurityStamp(ctx context.Context, userID uuid.UUID, newStamp string) error
}
SecurityStampStore rotates a user's security stamp. A stamp is snapshotted onto each session at issuance and compared during ValidateSession; rotating it forces every prior session to fail its next validation (global logout).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the authentication/authorization facade. Construct it with New, passing a Store (see ./postgres). Enable optional capabilities with the With* options: WithAuthStore (login/issuance), WithMfaMethods (MFA), WithPasswordPolicy, WithLockout, WithAudit, WithSecurityStamps.
func New ¶
New builds a Service, filling any unset Config fields with defaults. Pass the With* options to enable login, MFA, password policy, lockout, and audit.
func (*Service) Audit ¶
func (s *Service) Audit() *AuditService
Audit returns the configured AuditService, or nil if WithAudit was not used.
func (*Service) ChangePassword ¶
func (s *Service) ChangePassword(ctx context.Context, userID uuid.UUID, currentPassword, newPassword string) error
ChangePassword verifies the current password, enforces the password policy on the new one, updates the hash, and rotates the security stamp (global logout of other sessions). Requires WithAuthStore over a store implementing PasswordStore.
func (*Service) CookieName ¶
CookieName returns the configured session cookie name.
func (*Service) CreateUser ¶
CreateUser provisions a local user with a bcrypt-hashed password, enforcing the password policy when configured (WithPasswordPolicy).
func (*Service) HasPermission ¶
HasPermission reports whether the principal in ctx holds the permission key. It returns false when no principal is present. Pair it with SessionEnforcement, which injects the principal.
func (*Service) InvalidateUserSessions ¶
InvalidateUserSessions rotates the user's security stamp, forcing global logout of every session issued before this call (each is rejected on its next validation). Requires WithSecurityStamps.
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, in LoginInput) (*LoginResult, error)
Login verifies the account is not locked out, then the password (and MFA, if the user has it enabled), issues an opaque session, and returns the session token + authenticated Principal. It returns ErrAccountLocked, ErrMFARequired, or ErrInvalidCredentials as appropriate. Lockout/audit are applied when the corresponding options are configured.
func (*Service) RequireContextPermission ¶
RequireContextPermission returns ErrNoPrincipal if ctx has no principal, or a permission error string if the principal lacks the key. It is a convenience for non-HTTP call sites (jobs, gRPC) that still want the same permission gate.
func (*Service) RequirePermission ¶
RequirePermission wraps a handler, returning 403 unless the request's principal holds the permission key (401 if there is no principal). Use it after SessionEnforcement.
func (*Service) SessionEnforcement ¶
SessionEnforcement validates the opaque session cookie on every request and injects the resulting Principal into the request context. On any failure it responds 401. It is the Go analogue of UlinziLib's SessionEnforcementMiddleware and composes as standard net/http middleware (works with chi, stdlib, etc.).
func (*Service) UnlockAccount ¶
UnlockAccount clears any brute-force lockout on the email and resets the progressive escalation (admin action). It requires WithLockout.
func (*Service) ValidateSession ¶
ValidateSession resolves an opaque session cookie value into a Principal, mirroring UlinziLib's SessionEnforcementMiddleware:
- look up the session row by its opaque id;
- reject revoked (logged-out) sessions;
- reject on absolute expiry (token_expires_at, or created_at + session timeout);
- reject on idle expiry (now - last_activity > idle timeout);
- reject if the user is inactive;
- resolve roles + permission keys into the Principal;
- throttled write-back of last_activity (best effort).
It returns one of ErrSessionNotFound, ErrSessionRevoked, ErrSessionExpired, or ErrUserInactive on failure.
type SessionCapStore ¶
type SessionCapStore interface {
EnforceSessionCap(ctx context.Context, userID uuid.UUID, max int) error
}
SessionCapStore optionally enforces the per-user session cap (MaxSessions): after a new session is created, sessions beyond the newest max are evicted. AuthStore implementations may also satisfy this; Login uses it opportunistically.
type SessionPolicy ¶
type SessionPolicy struct {
SessionTimeout time.Duration
IdleTimeout time.Duration
MaxSessions int
}
SessionPolicy mirrors security.session_policies (durations, not raw minutes).
type SessionRecord ¶
type SessionRecord struct {
ID uuid.UUID
UserID uuid.UUID
SessionID string
CreatedAt time.Time
LastActivityAt time.Time
TokenExpiresAt *time.Time
LoggedOutAt *time.Time
IPAddress string // set at issuance; not used by validation
UserAgent string // set at issuance; not used by validation
// SecurityStamp is the user's security stamp captured at issuance. Compared
// against the user's current stamp during validation to force global logout.
SecurityStamp string
}
SessionRecord mirrors the validation-relevant columns of security.http_session_records. The opaque session cookie value is SessionID.
type Store ¶
type Store interface {
// GetSessionByID looks up a session by its opaque cookie value (session_id).
GetSessionByID(ctx context.Context, sessionID string) (*SessionRecord, error)
// TouchSession updates a session's last_activity_at (throttled by the caller).
TouchSession(ctx context.Context, id uuid.UUID, at time.Time) error
// GetActiveSessionPolicy returns the most recent session policy, or nil.
GetActiveSessionPolicy(ctx context.Context) (*SessionPolicy, error)
// GetUserByID returns the user, or nil if not found.
GetUserByID(ctx context.Context, id uuid.UUID) (*User, error)
// GetUserPermissions returns the resolved permission keys for a user
// (role grants UNION team grants).
GetUserPermissions(ctx context.Context, userID uuid.UUID) ([]string, error)
// GetUserRoles returns the user's (non-expired) role names.
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]string, error)
}
Store is the data dependency for wave-1: opaque-session validation and permission resolution against the "security" schema. The PostgreSQL implementation lives in ./postgres.
Implementations should return ErrSessionNotFound when a session row is absent and (nil, nil) when an optional lookup (policy, user) finds nothing.
type TOTPOption ¶
type TOTPOption func(*TOTPService)
TOTPOption configures optional TOTPService behaviour.
func TOTPWithReplayGuard ¶
func TOTPWithReplayGuard(u MfaUsageStore) TOTPOption
TOTPWithReplayGuard rejects re-use of a valid TOTP code within its window.
func TOTPWithSecretProtector ¶
func TOTPWithSecretProtector(p MfaSecretProtector) TOTPOption
TOTPWithSecretProtector encrypts the TOTP shared secret at rest.
type TOTPService ¶
type TOTPService struct {
// contains filtered or unexported fields
}
TOTPService is the default authenticator-app factor: TOTP verification plus single-use recovery codes, backed by github.com/pquerna/otp. It implements MfaMethod (Name "totp") and also exposes enrolment/management methods. The TOTP secret is encrypted at rest via an MfaSecretProtector, and successful TOTP verifications are recorded in an optional replay ledger.
func NewTOTPService ¶
func NewTOTPService(store MfaStore, issuer string, opts ...TOTPOption) *TOTPService
NewTOTPService builds a TOTPService. issuer is the label shown in authenticator apps (e.g. "ShughuliYangu"); it defaults to "UlinziLib". Without TOTPWithSecretProtector the secret is stored in plaintext (dev only).
func (*TOTPService) EnrollTOTP ¶
func (s *TOTPService) EnrollTOTP(ctx context.Context, userID uuid.UUID) (EnrollResult, error)
EnrollTOTP generates a fresh TOTP secret + recovery codes for the user and persists them (disabled until the first successful VerifyTOTP). The returned plaintext secret + codes must be surfaced to the user immediately.
func (*TOTPService) IsEnrolled ¶
IsEnrolled implements MfaMethod: the user has a confirmed authenticator.
func (*TOTPService) Verify ¶
Verify implements MfaMethod: validates a TOTP code (with replay guard).
func (*TOTPService) VerifyRecoveryCode ¶
func (s *TOTPService) VerifyRecoveryCode(ctx context.Context, userID uuid.UUID, code string) (bool, error)
VerifyRecoveryCode checks a recovery code and consumes it on success.
func (*TOTPService) VerifyTOTP ¶
VerifyTOTP validates a 6-digit code. The first successful verification flips two_factor_enabled to true (completing enrolment). A valid code is claimed in the replay ledger (when configured) so it cannot be reused within its window.
type TrustedDeviceService ¶
type TrustedDeviceService interface {
Trust(ctx context.Context, userID, deviceToken string) error
IsTrusted(ctx context.Context, userID, deviceToken string) (bool, error)
}
TrustedDeviceService remembers a device so MFA can be skipped on it later ("remember this device").
type UnimplementedIPWhitelistService ¶
type UnimplementedIPWhitelistService struct{}
UnimplementedIPWhitelistService is a roadmap placeholder.
type UnimplementedOIDCService ¶
type UnimplementedOIDCService struct{}
UnimplementedOIDCService is a roadmap placeholder.
func (UnimplementedOIDCService) Exchange ¶
func (UnimplementedOIDCService) Exchange(context.Context, IdentityProvider, string, string) (string, error)
type UnimplementedTrustedDeviceService ¶
type UnimplementedTrustedDeviceService struct{}
UnimplementedTrustedDeviceService is a roadmap placeholder.
type UnimplementedWebAuthnService ¶
type UnimplementedWebAuthnService struct{}
UnimplementedWebAuthnService is a roadmap placeholder.
func (UnimplementedWebAuthnService) BeginLogin ¶
func (UnimplementedWebAuthnService) BeginRegistration ¶
func (UnimplementedWebAuthnService) FinishLogin ¶
func (UnimplementedWebAuthnService) FinishRegistration ¶
type User ¶
type User struct {
ID uuid.UUID
Email string
UserName string
IsActive bool
// SecurityStamp is the user's current security stamp (security.users.security_stamp).
// A change invalidates every session issued before the change.
SecurityStamp string
}
User is the subset of security.users needed to build a Principal.
type UserAuth ¶
type UserAuth struct {
ID uuid.UUID
Email string
PasswordHash string
IsActive bool
SecurityStamp string // snapshotted onto the session for global-logout checks
}
UserAuth is the credential view of a user used during login.
type WebAuthnService ¶
type WebAuthnService interface {
BeginRegistration(ctx context.Context, userID string) (options []byte, err error)
FinishRegistration(ctx context.Context, userID string, attestation []byte) error
BeginLogin(ctx context.Context, userID string) (options []byte, err error)
FinishLogin(ctx context.Context, userID string, assertion []byte) error
}
WebAuthnService manages passkey (FIDO2) registration and assertion.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package migrate embeds ulinzilib-go's "security"-schema goose migrations and applies them to a PostgreSQL database.
|
Package migrate embeds ulinzilib-go's "security"-schema goose migrations and applies them to a PostgreSQL database. |
|
Package postgres provides the PostgreSQL-backed ulinzi.Store implementation (wave-1: opaque-session validation + permission resolution) over the "security" schema, using pgx.
|
Package postgres provides the PostgreSQL-backed ulinzi.Store implementation (wave-1: opaque-session validation + permission resolution) over the "security" schema, using pgx. |