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).
- External IdP linkage (WithExternalLogins, Service.ResolveExternalLogin), IdP configuration (IdpConfigService), and WebAuthn/passkey ceremonies (WebAuthnService).
Entities live in the sibling domain package (github.com/Cyrus-0101/ulinzilib-go/domain) and are re-exported here as aliases for API stability. Remaining roadmap surfaces (OIDC token exchange, 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 AESGCMIdpSecretProtector
- func (p *AESGCMIdpSecretProtector) ProtectClientSecret(provider, plaintext string) (string, error)
- func (p *AESGCMIdpSecretProtector) ProtectServiceUserToken(provider, plaintext string) (string, error)
- func (p *AESGCMIdpSecretProtector) ProtectZitadelClientID(plaintext string) (string, error)
- func (p *AESGCMIdpSecretProtector) TryUnprotectClientSecret(provider, ciphertext string) (string, bool)
- func (p *AESGCMIdpSecretProtector) TryUnprotectServiceUserToken(provider, ciphertext string) (string, bool)
- func (p *AESGCMIdpSecretProtector) TryUnprotectZitadelClientID(ciphertext string) (string, bool)
- type AESGCMSecretProtector
- type ActiveLockout
- type AppUserMfa
- type AppUserPasskey
- type AuditEvent
- type AuditService
- type AuditStore
- type AuthStore
- type ChallengeStore
- type Config
- type DefaultHibpService
- type EnrollResult
- type ExternalLoginStore
- type HibpService
- type IPWhitelistService
- type IdentityProvider
- type IdpConfigInfo
- type IdpConfigService
- func (s *IdpConfigService) DecryptedClientSecret(ctx context.Context, provider string) (secret string, ok bool, err error)
- func (s *IdpConfigService) DecryptedServiceUserToken(ctx context.Context, provider string) (token string, ok bool, err error)
- func (s *IdpConfigService) DecryptedZitadelClientID(ctx context.Context) (clientID string, ok bool, err error)
- func (s *IdpConfigService) Get(ctx context.Context, provider string) (*IdpConfigInfo, error)
- func (s *IdpConfigService) List(ctx context.Context) ([]IdpConfigInfo, error)
- func (s *IdpConfigService) RawClientID(ctx context.Context, provider string) (*string, error)
- func (s *IdpConfigService) Upsert(ctx context.Context, provider string, in UpsertIdpConfigInput, ...) (*IdpConfigInfo, error)
- type IdpConfigStore
- type IdpConfiguration
- type IdpSecretProtector
- type InMemoryChallengeStore
- 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 Mfadeprecated
- type MfaMethod
- type MfaSecretProtector
- type MfaStore
- type MfaUsageStore
- type NewUser
- type NoopHibpService
- type NoopIdpSecretProtector
- func (NoopIdpSecretProtector) ProtectClientSecret(_, plaintext string) (string, error)
- func (NoopIdpSecretProtector) ProtectServiceUserToken(_, plaintext string) (string, error)
- func (NoopIdpSecretProtector) ProtectZitadelClientID(plaintext string) (string, error)
- func (NoopIdpSecretProtector) TryUnprotectClientSecret(_, ciphertext string) (string, bool)
- func (NoopIdpSecretProtector) TryUnprotectServiceUserToken(_, ciphertext string) (string, bool)
- func (NoopIdpSecretProtector) TryUnprotectZitadelClientID(ciphertext string) (string, bool)
- type NoopSecretProtector
- type OIDCService
- type Option
- func WithAudit(store AuditStore) Option
- func WithAuthStore(a AuthStore) Option
- func WithExternalLogins(store ExternalLoginStore) 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 PasskeyStore
- 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) LinkExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string, ...) error
- func (s *Service) ListExternalLogins(ctx context.Context, userID uuid.UUID) ([]UserLogin, 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) ResolveExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (*User, error)
- func (s *Service) SessionEnforcement(next http.Handler) http.Handler
- func (s *Service) UnlinkExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (bool, error)
- 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 UpsertIdpConfigInput
- type User
- type UserAuth
- type UserLogin
- type WebAuthnConfig
- type WebAuthnService
- func (s *WebAuthnService) BeginLogin(ctx context.Context, userID uuid.UUID, name, displayName string) ([]byte, error)
- func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID uuid.UUID, name, displayName string) ([]byte, error)
- func (s *WebAuthnService) FinishLogin(ctx context.Context, userID uuid.UUID, name, displayName string, ...) (*AppUserPasskey, error)
- func (s *WebAuthnService) FinishRegistration(ctx context.Context, userID uuid.UUID, name, displayName string, ...) (*AppUserPasskey, error)
- func (s *WebAuthnService) ListPasskeys(ctx context.Context, userID uuid.UUID) ([]AppUserPasskey, error)
- func (s *WebAuthnService) RemovePasskey(ctx context.Context, userID uuid.UUID, credentialID string) (bool, error)
Constants ¶
const ( ProviderLocal = domain.ProviderLocal ProviderZitadel = domain.ProviderZitadel ProviderEntra = domain.ProviderEntra ProviderGoogle = domain.ProviderGoogle )
Identity-provider constants, re-exported from the domain layer.
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 ( // when the Service was built without WithExternalLogins. ErrExternalLoginUnavailable = errors.New("ulinzi: external-login store not configured (use WithExternalLogins)") // ErrExternalLoginNotFound is returned by ResolveExternalLogin when no local // user is linked to the given (provider, providerKey). ErrExternalLoginNotFound = errors.New("ulinzi: external login not found") )
External-login errors.
var ( // ErrWebAuthnChallengeMissing is returned when a Finish* call runs without a // matching Begin* (no cached challenge, or it expired). Fail closed. ErrWebAuthnChallengeMissing = errors.New("ulinzi: webauthn challenge missing or expired") // ErrWebAuthnCredentialExists is returned when registration produces a // credential id that is already registered. ErrWebAuthnCredentialExists = errors.New("ulinzi: passkey credential already registered") )
WebAuthn ceremony errors.
var ErrIdpProviderRequired = errors.New("ulinzi: idp provider is required")
ErrIdpProviderRequired is returned when an IdP config operation is called with a blank provider key.
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 ErrPasskeyNotFound = errors.New("ulinzi: passkey not found")
ErrPasskeyNotFound is returned when a passkey lookup finds no matching credential.
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 AESGCMIdpSecretProtector ¶ added in v0.4.0
type AESGCMIdpSecretProtector struct {
// contains filtered or unexported fields
}
AESGCMIdpSecretProtector implements IdpSecretProtector with AES-256-GCM. The stored form is "v1:" + base64std(nonce || ciphertext || tag) — identical to AESGCMSecretProtector, but with a per-purpose GCM AAD.
func NewAESGCMIdpSecretProtector ¶ added in v0.4.0
func NewAESGCMIdpSecretProtector(key []byte) (*AESGCMIdpSecretProtector, error)
NewAESGCMIdpSecretProtector builds a protector from a 16/24/32-byte key (32 = AES-256, recommended). Source it from a KMS/secret manager, not source code. Prefer a key independent of the MFA protector's key.
func (*AESGCMIdpSecretProtector) ProtectClientSecret ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) ProtectClientSecret(provider, plaintext string) (string, error)
func (*AESGCMIdpSecretProtector) ProtectServiceUserToken ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) ProtectServiceUserToken(provider, plaintext string) (string, error)
func (*AESGCMIdpSecretProtector) ProtectZitadelClientID ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) ProtectZitadelClientID(plaintext string) (string, error)
func (*AESGCMIdpSecretProtector) TryUnprotectClientSecret ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) TryUnprotectClientSecret(provider, ciphertext string) (string, bool)
func (*AESGCMIdpSecretProtector) TryUnprotectServiceUserToken ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) TryUnprotectServiceUserToken(provider, ciphertext string) (string, bool)
func (*AESGCMIdpSecretProtector) TryUnprotectZitadelClientID ¶ added in v0.4.0
func (p *AESGCMIdpSecretProtector) TryUnprotectZitadelClientID(ciphertext string) (string, bool)
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 ¶
type ActiveLockout = domain.ActiveLockout
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type AppUserMfa ¶ added in v0.4.0
type AppUserMfa = domain.AppUserMfa
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type AppUserPasskey ¶ added in v0.4.0
type AppUserPasskey = domain.AppUserPasskey
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type AuditEvent ¶
type AuditEvent = domain.AuditEvent
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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 ChallengeStore ¶ added in v0.4.0
type ChallengeStore interface {
// Save stores data for (userID, purpose) with a TTL, replacing any prior value.
Save(ctx context.Context, userID uuid.UUID, purpose string, data []byte, ttl time.Duration) error
// Take atomically retrieves and DELETES the stored data for (userID, purpose).
// ok=false means nothing was stored (or it expired) — the challenge is single-use.
Take(ctx context.Context, userID uuid.UUID, purpose string) (data []byte, ok bool, err error)
}
ChallengeStore persists the per-user WebAuthn ceremony challenge (SessionData) between the Begin and Finish steps. A Finish with no stored challenge MUST fail closed. The in-process InMemoryChallengeStore is fine for a single replica; multi-replica deployments should back this with a shared store (e.g. Redis).
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 ExternalLoginStore ¶ added in v0.4.0
type ExternalLoginStore interface {
// GetUserIDByExternalLogin resolves the local user id for an external subject.
// It returns ok=false (and uuid.Nil) when no link exists.
GetUserIDByExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (userID uuid.UUID, ok bool, err error)
// LinkExternalLogin creates or updates the link for (LoginProvider, ProviderKey),
// pointing it at UserID. It is an idempotent upsert on the composite key.
LinkExternalLogin(ctx context.Context, login UserLogin) error
// UnlinkExternalLogin removes the (provider, providerKey) link, reporting
// whether a row was removed.
UnlinkExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (removed bool, err error)
// ListExternalLogins returns all external links for a user.
ListExternalLogins(ctx context.Context, userID uuid.UUID) ([]UserLogin, error)
}
ExternalLoginStore persists external identity-provider links (security.user_logins) — the replacement for the removed users.zitadel_user_id / entra_user_id / google_user_id columns. It maps an external provider subject to a local user (one row per provider+key).
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 = domain.IdentityProvider
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type IdpConfigInfo ¶ added in v0.4.0
type IdpConfigInfo = domain.IdpConfigInfo
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type IdpConfigService ¶ added in v0.4.0
type IdpConfigService struct {
// contains filtered or unexported fields
}
IdpConfigService reads and writes identity-provider configuration, owning the IdpSecretProtector so callers never handle ciphertext. It mirrors the .NET IdpConfigurationRepository: reads return a ciphertext-free projection (IdpConfigInfo), the write path is a partial-update upsert, and the decrypt helpers fail soft. Provider keys are normalized to lowercase.
func NewIdpConfigService ¶ added in v0.4.0
func NewIdpConfigService(store IdpConfigStore, protector IdpSecretProtector) *IdpConfigService
NewIdpConfigService builds the service. A nil protector uses NoopIdpSecretProtector (dev/tests only — secrets are then stored in plaintext).
func (*IdpConfigService) DecryptedClientSecret ¶ added in v0.4.0
func (s *IdpConfigService) DecryptedClientSecret(ctx context.Context, provider string) (secret string, ok bool, err error)
DecryptedClientSecret returns the provider's decrypted client secret. ok=false means it is unset or undecryptable (fail soft).
func (*IdpConfigService) DecryptedServiceUserToken ¶ added in v0.4.0
func (s *IdpConfigService) DecryptedServiceUserToken(ctx context.Context, provider string) (token string, ok bool, err error)
DecryptedServiceUserToken returns the provider's decrypted service-user token.
func (*IdpConfigService) DecryptedZitadelClientID ¶ added in v0.4.0
func (s *IdpConfigService) DecryptedZitadelClientID(ctx context.Context) (clientID string, ok bool, err error)
DecryptedZitadelClientID returns the decrypted Zitadel client id (Zitadel is the only provider whose client_id column is stored encrypted).
func (*IdpConfigService) Get ¶ added in v0.4.0
func (s *IdpConfigService) Get(ctx context.Context, provider string) (*IdpConfigInfo, error)
Get returns the ciphertext-free view of a provider's config, or nil if none.
func (*IdpConfigService) List ¶ added in v0.4.0
func (s *IdpConfigService) List(ctx context.Context) ([]IdpConfigInfo, error)
List returns the ciphertext-free views of every configured provider.
func (*IdpConfigService) RawClientID ¶ added in v0.4.0
RawClientID returns the client_id column verbatim (still ciphertext for zitadel), for callers that decrypt on their own schedule. It returns nil when the value is unset or no row exists.
func (*IdpConfigService) Upsert ¶ added in v0.4.0
func (s *IdpConfigService) Upsert(ctx context.Context, provider string, in UpsertIdpConfigInput, userID *uuid.UUID) (*IdpConfigInfo, error)
Upsert applies a partial update to a provider's config, encrypting any supplied secrets, and returns the resulting ciphertext-free view.
type IdpConfigStore ¶ added in v0.4.0
type IdpConfigStore interface {
// GetIdpConfigByProvider returns the config for a provider, or nil if none.
GetIdpConfigByProvider(ctx context.Context, provider string) (*IdpConfiguration, error)
// ListIdpConfigs returns all configs ordered by provider.
ListIdpConfigs(ctx context.Context) ([]IdpConfiguration, error)
// UpsertIdpConfig inserts or replaces the row for c.Provider (a full write of
// the already-merged, already-encrypted entity).
UpsertIdpConfig(ctx context.Context, c *IdpConfiguration) error
}
IdpConfigStore persists identity-provider configuration (security.idp_configurations), one row per provider. The store round-trips the full IdpConfiguration (ciphertext columns included); encryption and the partial-update merge are applied above it by IdpConfigService.
type IdpConfiguration ¶ added in v0.4.0
type IdpConfiguration = domain.IdpConfiguration
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type IdpSecretProtector ¶ added in v0.4.0
type IdpSecretProtector interface {
ProtectClientSecret(provider, plaintext string) (string, error)
TryUnprotectClientSecret(provider, ciphertext string) (plaintext string, ok bool)
ProtectServiceUserToken(provider, plaintext string) (string, error)
TryUnprotectServiceUserToken(provider, ciphertext string) (plaintext string, ok bool)
ProtectZitadelClientID(plaintext string) (string, error)
TryUnprotectZitadelClientID(ciphertext string) (plaintext string, ok bool)
}
IdpSecretProtector encrypts the sensitive columns of an IdpConfiguration (the client secret, the service-user token, and the Zitadel client id) at rest. Each value is bound to a DISTINCT purpose (GCM AAD) so ciphertext cannot be moved between fields or providers. The Try* readers FAIL SOFT: on any decrypt/format error (including a rotated key) they return ("", false) rather than an error, so "is this configured?" callers degrade gracefully — mirroring the .NET IIdpSecretProtector.TryUnprotect* contract. There is deliberately no legacy-plaintext fallback (unlike MFA secrets): IdP secrets fail closed.
type InMemoryChallengeStore ¶ added in v0.4.0
type InMemoryChallengeStore struct {
// contains filtered or unexported fields
}
InMemoryChallengeStore is a process-local ChallengeStore with per-entry TTL. It is safe for a single replica; use a shared backend for multi-replica setups.
func NewInMemoryChallengeStore ¶ added in v0.4.0
func NewInMemoryChallengeStore() *InMemoryChallengeStore
NewInMemoryChallengeStore returns an empty in-process challenge store.
type LockoutPolicy ¶
type LockoutPolicy = domain.LockoutPolicy
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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 = domain.LoginAttempt
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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
deprecated
type Mfa = domain.AppUserMfa
Mfa is the former name of AppUserMfa.
Deprecated: use AppUserMfa (the entity is 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) (*AppUserMfa, error) // nil, nil if none
UpsertMfa(ctx context.Context, m *AppUserMfa) error
}
MfaStore persists MFA enrolment state (security.app_user_mfa). It reads and writes the AppUserMfa aggregate (defined in the domain package).
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 NoopIdpSecretProtector ¶ added in v0.4.0
type NoopIdpSecretProtector struct{}
NoopIdpSecretProtector stores secrets verbatim (no encryption). It is the default when no protector is configured and is intended for tests/dev ONLY — it leaves IdP secrets readable in the database. Configure an AESGCMIdpSecretProtector in production.
func (NoopIdpSecretProtector) ProtectClientSecret ¶ added in v0.4.0
func (NoopIdpSecretProtector) ProtectClientSecret(_, plaintext string) (string, error)
func (NoopIdpSecretProtector) ProtectServiceUserToken ¶ added in v0.4.0
func (NoopIdpSecretProtector) ProtectServiceUserToken(_, plaintext string) (string, error)
func (NoopIdpSecretProtector) ProtectZitadelClientID ¶ added in v0.4.0
func (NoopIdpSecretProtector) ProtectZitadelClientID(plaintext string) (string, error)
func (NoopIdpSecretProtector) TryUnprotectClientSecret ¶ added in v0.4.0
func (NoopIdpSecretProtector) TryUnprotectClientSecret(_, ciphertext string) (string, bool)
func (NoopIdpSecretProtector) TryUnprotectServiceUserToken ¶ added in v0.4.0
func (NoopIdpSecretProtector) TryUnprotectServiceUserToken(_, ciphertext string) (string, bool)
func (NoopIdpSecretProtector) TryUnprotectZitadelClientID ¶ added in v0.4.0
func (NoopIdpSecretProtector) TryUnprotectZitadelClientID(ciphertext string) (string, bool)
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 WithExternalLogins ¶ added in v0.4.0
func WithExternalLogins(store ExternalLoginStore) Option
WithExternalLogins enables external identity-provider linkage: resolving, linking, and unlinking external logins against security.user_logins. The PostgreSQL Store satisfies ExternalLoginStore.
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 PasskeyStore ¶ added in v0.4.0
type PasskeyStore interface {
// GetPasskeyByCredentialID looks up a passkey by its globally-unique credential
// id — the sign-in lookup. Returns (nil, nil) if none.
GetPasskeyByCredentialID(ctx context.Context, credentialID string) (*AppUserPasskey, error)
// ListPasskeysByUserID returns a user's passkeys, oldest first.
ListPasskeysByUserID(ctx context.Context, userID uuid.UUID) ([]AppUserPasskey, error)
// AddPasskey inserts a new passkey. A duplicate credential_id must return an error.
AddPasskey(ctx context.Context, p *AppUserPasskey) error
// UpdatePasskeySignCount persists the monotonic signature counter after a
// successful assertion.
UpdatePasskeySignCount(ctx context.Context, id uuid.UUID, signCount uint32) error
// RemovePasskeyByUserAndCredentialID deletes a user's passkey, reporting whether
// a row was removed.
RemovePasskeyByUserAndCredentialID(ctx context.Context, userID uuid.UUID, credentialID string) (removed bool, err error)
}
PasskeyStore persists WebAuthn passkey credentials (security.app_user_passkeys). It reads and writes the AppUserPasskey aggregate; a user may own many passkeys. Reads return (nil, nil) / an empty slice when nothing matches.
type PasswordPolicy ¶
type PasswordPolicy = domain.PasswordPolicy
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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 ¶
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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) LinkExternalLogin ¶ added in v0.4.0
func (s *Service) LinkExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string, displayName *string, userID uuid.UUID) error
LinkExternalLogin links an external provider subject to a local user (idempotent on the composite key). Requires WithExternalLogins.
func (*Service) ListExternalLogins ¶ added in v0.4.0
ListExternalLogins lists a user's external identity links. Requires WithExternalLogins.
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) ResolveExternalLogin ¶ added in v0.4.0
func (s *Service) ResolveExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (*User, error)
ResolveExternalLogin returns the local user linked to an external provider subject. It returns ErrExternalLoginNotFound when the subject is not linked (or the linked user no longer exists) and ErrUserInactive when the linked user is disabled. This is the Go analogue of the .NET IUserRepository.GetByZitadelUserIdAsync, generalized to any provider. Requires WithExternalLogins.
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) UnlinkExternalLogin ¶ added in v0.4.0
func (s *Service) UnlinkExternalLogin(ctx context.Context, provider IdentityProvider, providerKey string) (bool, error)
UnlinkExternalLogin removes an external provider link, reporting whether a row was removed. Requires WithExternalLogins.
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 = domain.SessionPolicy
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type SessionRecord ¶
type SessionRecord = domain.SessionRecord
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
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 UpsertIdpConfigInput ¶ added in v0.4.0
type UpsertIdpConfigInput struct {
DisplayName *string
ClientID *string // plaintext; encrypted on write for the zitadel provider
TenantID *string
DiscoveryURL *string
MetadataJSON *string
ClientSecret *string // plaintext; encrypted on write when non-blank
ServiceUserToken *string // plaintext; encrypted on write when non-blank
}
UpsertIdpConfigInput carries a partial update to an IdP configuration. A nil pointer leaves the existing value unchanged; a non-nil pointer overwrites. ClientSecret and ServiceUserToken are PLAINTEXT inputs — the service encrypts them on write, and a blank value KEEPS the existing secret (never clears it), mirroring the .NET UpsertAsync semantics.
type User ¶
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type UserAuth ¶
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type UserLogin ¶ added in v0.4.0
The canonical entity and value types live in the domain package (the DDD domain layer). The aliases below re-export them under this package's names so existing callers, the postgres adapter, and the tests keep compiling — the alias and the domain type are the SAME type. domain is the single source of truth; new code may reference either name.
type WebAuthnConfig ¶ added in v0.4.0
type WebAuthnConfig struct {
RPID string
RPDisplayName string
RPOrigins []string
// ChallengeTTL bounds how long a Begin* challenge is valid before its Finish*
// must arrive. Defaults to 5 minutes.
ChallengeTTL time.Duration
}
WebAuthnConfig is the relying-party configuration for passkeys. RPID is the effective domain (e.g. "example.com"); RPOrigins are the fully-qualified origins permitted to complete ceremonies (e.g. "https://app.example.com"). These should come from the auth-settings source, not be hard-coded.
type WebAuthnService ¶
type WebAuthnService struct {
// contains filtered or unexported fields
}
WebAuthnService runs the FIDO2/WebAuthn registration and assertion ceremonies over a PasskeyStore and a ChallengeStore, using github.com/go-webauthn. It replaces the former roadmap stub with a real implementation: attestation and assertion are cryptographically verified, credential ids are base64url, the monotonic sign counter is persisted on each assertion, and Finish* fails closed without a matching Begin*.
func NewWebAuthnService ¶ added in v0.4.0
func NewWebAuthnService(store PasskeyStore, sessions ChallengeStore, cfg WebAuthnConfig) (*WebAuthnService, error)
NewWebAuthnService builds the service. A nil ChallengeStore uses an in-process InMemoryChallengeStore. It errors if the relying-party config is invalid.
func (*WebAuthnService) BeginLogin ¶
func (s *WebAuthnService) BeginLogin(ctx context.Context, userID uuid.UUID, name, displayName string) ([]byte, error)
BeginLogin starts a passkey assertion ceremony for a known user. It returns the JSON for navigator.credentials.get() and caches the challenge. It returns ErrPasskeyNotFound when the user has no passkeys.
func (*WebAuthnService) BeginRegistration ¶
func (s *WebAuthnService) BeginRegistration(ctx context.Context, userID uuid.UUID, name, displayName string) ([]byte, error)
BeginRegistration starts a passkey enrolment ceremony. It returns the JSON to hand to navigator.credentials.create() and caches the challenge for FinishReg- istration. The user's existing passkeys are excluded so a device cannot enrol twice.
func (*WebAuthnService) FinishLogin ¶
func (s *WebAuthnService) FinishLogin(ctx context.Context, userID uuid.UUID, name, displayName string, response []byte) (*AppUserPasskey, error)
FinishLogin verifies the authenticator's assertion response (the raw JSON body from navigator.credentials.get()), advances the stored sign counter, and returns the matched passkey. The credential must belong to userID.
func (*WebAuthnService) FinishRegistration ¶
func (s *WebAuthnService) FinishRegistration(ctx context.Context, userID uuid.UUID, name, displayName string, response []byte, label string) (*AppUserPasskey, error)
FinishRegistration verifies the authenticator's attestation response (the raw JSON body from navigator.credentials.create()), stores the new passkey, and returns it. label is an optional user-facing name for the credential.
func (*WebAuthnService) ListPasskeys ¶ added in v0.4.0
func (s *WebAuthnService) ListPasskeys(ctx context.Context, userID uuid.UUID) ([]AppUserPasskey, error)
ListPasskeys returns a user's registered passkeys, oldest first.
func (*WebAuthnService) RemovePasskey ¶ added in v0.4.0
func (s *WebAuthnService) RemovePasskey(ctx context.Context, userID uuid.UUID, credentialID string) (bool, error)
RemovePasskey removes one of a user's passkeys by credential id, reporting whether a row was removed.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package domain holds ulinzilib-go's pure entity and value types — the DDD domain layer, a faithful port of Adelphi.SecurityLib's Domain/Entities.
|
Package domain holds ulinzilib-go's pure entity and value types — the DDD domain layer, a faithful port of Adelphi.SecurityLib's Domain/Entities. |
|
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. |