Documentation
¶
Overview ¶
Package services holds the goravel-authkit business logic: credential verification, password changes with other-session invalidation, user management, and audit writes. Services never touch the HTTP session — that is the controller's concern; they only verify credentials and mutate tables.
Passwords are hashed via the Goravel Hash facade (configure bcrypt cost 12 in config/hashing.go). No password or hash is ever logged.
Index ¶
- Constants
- Variables
- type Audit
- type AuditEntry
- type Auth
- func (s *Auth) Authenticate(ctx context.Context, email, password string) (*models.User, error)
- func (s *Auth) ChangePassword(ctx context.Context, id uuid.UUID, currentPassword, newPassword string) (time.Time, error)
- func (s *Auth) Me(ctx context.Context, id uuid.UUID) (*models.User, error)
- func (s *Auth) UpdateProfile(ctx context.Context, id uuid.UUID, email, name string) (*models.User, bool, error)
- type Crypter
- type Enrollment
- type FacadeCrypter
- type FacadeHasher
- type Hasher
- type Remember
- func (s *Remember) Issue(ctx context.Context, userID uuid.UUID) (string, error)
- func (s *Remember) PruneExpired(ctx context.Context) error
- func (s *Remember) Resolve(ctx context.Context, cookieValue string) (uuid.UUID, string, error)
- func (s *Remember) Revoke(ctx context.Context, cookieValue string) error
- func (s *Remember) RevokeAllForUser(ctx context.Context, userID uuid.UUID) error
- func (s *Remember) TTL() time.Duration
- type SessionView
- type Sessions
- func (s *Sessions) Forget(ctx context.Context, sessionID string) error
- func (s *Sessions) ForgetAllForUser(ctx context.Context, userID uuid.UUID) error
- func (s *Sessions) List(ctx context.Context, userID uuid.UUID, currentSessionID string) ([]SessionView, error)
- func (s *Sessions) PruneStale(ctx context.Context) error
- func (s *Sessions) Terminate(ctx context.Context, userID, publicID uuid.UUID, currentSessionID string) error
- func (s *Sessions) TerminateOthers(ctx context.Context, userID uuid.UUID, currentSessionID string) error
- func (s *Sessions) Touch(ctx context.Context, sessionID string) (bool, error)
- func (s *Sessions) Track(ctx context.Context, sessionID string, userID uuid.UUID, ip, userAgent string) error
- type TwoFactor
- func (s *TwoFactor) Confirm(ctx context.Context, id uuid.UUID, code string) ([]string, error)
- func (s *TwoFactor) ConsumeRecoveryCode(ctx context.Context, id uuid.UUID, code string) (bool, error)
- func (s *TwoFactor) Disable(ctx context.Context, id uuid.UUID) error
- func (s *TwoFactor) Enable(ctx context.Context, id uuid.UUID) (*Enrollment, error)
- func (s *TwoFactor) RegenerateRecoveryCodes(ctx context.Context, id uuid.UUID) ([]string, error)
- func (s *TwoFactor) RemainingRecoveryCodes(ctx context.Context, id uuid.UUID) (int, error)
- func (s *TwoFactor) Verify(user *models.User, code string) (bool, error)
- func (s *TwoFactor) VerifyLoginCode(ctx context.Context, id uuid.UUID, code string) (bool, error)
- type Users
- func (s *Users) Create(ctx context.Context, email, name, password, role string) (*models.User, error)
- func (s *Users) Delete(ctx context.Context, id uuid.UUID) error
- func (s *Users) GetByID(ctx context.Context, id uuid.UUID) (*models.User, error)
- func (s *Users) List(ctx context.Context) ([]models.User, error)
- func (s *Users) SetPassword(ctx context.Context, id uuid.UUID, newPassword string) (*models.User, error)
- func (s *Users) Update(ctx context.Context, id uuid.UUID, email, name, role string, disabled *bool, ...) (*models.User, error)
Constants ¶
const AdminRole = "admin"
AdminRole is the canonical privileged role. It is the fail-closed default for the /users management gate and the bootstrap (auth:create-user) admin.
const DefaultMinPasswordLength = 8
DefaultMinPasswordLength is the fallback minimum new-password length when the app does not override authkit.min_password_length.
const DefaultRecoveryCodeCount = 8
DefaultRecoveryCodeCount is the fallback number of recovery codes generated on confirmation when the app does not override authkit.two_factor.recovery_codes.
const DefaultRememberLifetime = 30 * 24 * time.Hour
DefaultRememberLifetime is the fallback validity window for a remember cookie.
const DefaultRole = "user"
DefaultRole is the non-privileged role assigned to users created without an explicit role when no safer configured role can be derived. It must never be a management/admin role: new users default to the least privilege.
const DefaultSessionActiveWindow = 2 * time.Hour
DefaultSessionActiveWindow bounds how recently a session must have been active to count as "current" — mirrors the default session lifetime.
const DefaultTwoFactorIssuer = "goravel-authkit"
DefaultTwoFactorIssuer is the fallback issuer shown in the authenticator app.
const MaxPasswordBytes = 72
MaxPasswordBytes caps accepted passwords at bcrypt's hard limit: bcrypt only consumes the first 72 bytes, so anything longer is silently truncated (two distinct long passwords sharing a 72-byte prefix would compare equal). Rejecting oversize input up front makes that truncation impossible.
Variables ¶
var ( // ErrInvalidCredentials is returned for both unknown-email and wrong-password // so the response never reveals which. ErrInvalidCredentials = errors.New("invalid credentials") // ErrValidation is a generic input-validation failure; wrap it with a // human-readable message via errors.Join for detail. ErrValidation = errors.New("validation error") // ErrWrongPassword is returned when the supplied current password is wrong. ErrWrongPassword = errors.New("wrong password") // ErrNotFound is returned when a requested user does not exist. ErrNotFound = errors.New("not found") ErrUnauthorized = errors.New("unauthorized") // ErrAlreadyExists is returned when an email collides with an existing user. ErrAlreadyExists = errors.New("already exists") // ErrLastAdmin is returned when an operation (delete, disable, or demote) // would leave no active user holding a management/admin role. ErrLastAdmin = errors.New("cannot remove the last admin") // ErrInternal wraps an unexpected lower-layer failure. ErrInternal = errors.New("internal error") // ErrTwoFactorNotEnrolled is returned when a 2FA operation needs an active // (or pending) enrollment the user does not have. ErrTwoFactorNotEnrolled = errors.New("two-factor not enrolled") // ErrTwoFactorAlreadyEnabled is returned when enrolling a user who already // has 2FA confirmed. ErrTwoFactorAlreadyEnabled = errors.New("two-factor already enabled") // ErrInvalidCode is returned when a TOTP (or recovery) code does not verify. ErrInvalidCode = errors.New("invalid code") // ErrInvalidRememberToken is returned when a "remember me" cookie is malformed, // unknown, expired, or fails validation. ErrInvalidRememberToken = errors.New("invalid remember token") )
Sentinel errors. Controllers map these to HTTP status codes via errors.Is.
var LoginActions = []string{"auth.login", "auth.login_remember"}
LoginActions are the audit actions that count as a successful sign-in.
Functions ¶
This section is empty.
Types ¶
type Audit ¶
type Audit struct {
// contains filtered or unexported fields
}
Audit is the single chokepoint for writing audit entries.
func NewAudit ¶
func NewAudit(repo repositories.AuditRepository) *Audit
NewAudit builds the audit service.
type AuditEntry ¶
type AuditEntry struct {
ActorID *uuid.UUID
ActorEmail string
Action string
ResourceType string
ResourceID *string
Metadata map[string]any
IP string
}
AuditEntry is the input to the audit service — shaped like a future event payload so audit can move to an event/queue pipeline later without changing callers.
type Auth ¶
type Auth struct {
// contains filtered or unexported fields
}
Auth encapsulates credential verification and self-service password changes. Session establishment itself is the controller's concern.
func NewAuth ¶
func NewAuth(repo repositories.UsersRepository, hasher Hasher, minPwLen int) *Auth
NewAuth builds the auth service. minPwLen <= 0 falls back to DefaultMinPasswordLength.
func (*Auth) Authenticate ¶
Authenticate verifies credentials and returns the matching user. A generic ErrInvalidCredentials is returned for both unknown-email and wrong-password.
func (*Auth) ChangePassword ¶
func (s *Auth) ChangePassword(ctx context.Context, id uuid.UUID, currentPassword, newPassword string) (time.Time, error)
ChangePassword verifies the current password, validates the new one (min length, must differ), updates the hash, and bumps password_changed_at so every OTHER active session for this user is rejected on its next request. It returns the new password_changed_at — re-read from the DB so it matches the stored precision exactly — so the controller can re-stamp THIS session and keep it alive. (A locally-generated time.Now() has nanosecond precision, but Postgres timestamptz stores microseconds, so stamping the raw value would never match the DB value on the next request and would log THIS session out.)
func (*Auth) Me ¶
Me returns the authenticated user, or ErrUnauthorized if it no longer exists or has been disabled. The disabled check mirrors the Authenticated middleware so the method is safe to use as a standalone "load current user" primitive.
func (*Auth) UpdateProfile ¶
func (s *Auth) UpdateProfile(ctx context.Context, id uuid.UUID, email, name string) (*models.User, bool, error)
UpdateProfile lets the authenticated user change their own name and email. The role is intentionally out of scope (admin-managed via the users service). It returns the updated user and whether anything actually changed (false skips the write so a no-op save isn't audited). A colliding email returns ErrAlreadyExists; a missing email returns ErrValidation. Changing the email clears EmailVerified, since the new address is unverified.
type Crypter ¶
type Crypter interface {
Encrypt(value string) (string, error)
Decrypt(payload string) (string, error)
}
Crypter abstracts symmetric encryption so the two-factor service can be unit-tested without booting the framework. The default implementation delegates to the Goravel Crypt facade (which uses the app key).
type Enrollment ¶
Enrollment is returned when a user starts TOTP enrollment: the secret and the otpauth:// URL for rendering a QR code.
type FacadeCrypter ¶
type FacadeCrypter struct{}
FacadeCrypter is the production Crypter, backed by facades.Crypt().
func NewFacadeCrypter ¶
func NewFacadeCrypter() FacadeCrypter
NewFacadeCrypter returns a Crypter backed by the Goravel Crypt facade.
type FacadeHasher ¶
type FacadeHasher struct{}
FacadeHasher is the production Hasher, backed by facades.Hash().
func NewFacadeHasher ¶
func NewFacadeHasher() FacadeHasher
NewFacadeHasher returns a Hasher backed by the Goravel Hash facade.
func (FacadeHasher) Check ¶
func (FacadeHasher) Check(value, hashed string) bool
type Hasher ¶
Hasher abstracts password hashing so services can be unit-tested without booting the framework. The default implementation delegates to the Goravel Hash facade (configure bcrypt cost 12 in config/hashing.go).
type Remember ¶
type Remember struct {
// contains filtered or unexported fields
}
Remember implements persistent "remember me" login via the selector-validator pattern. The cookie value is "selector:validator"; the store keeps the selector in clear (indexed) and only a SHA-256 hash of the validator. The validator is rotated on every successful use.
func NewRemember ¶
func NewRemember(repo repositories.RememberRepository, ttl time.Duration) *Remember
NewRemember builds the remember service. ttl <= 0 falls back to DefaultRememberLifetime.
func (*Remember) Issue ¶
Issue creates a new remember token for the user and returns the cookie value ("selector:validator") to set on the response.
func (*Remember) PruneExpired ¶
PruneExpired deletes every remember token whose expiry has passed. Intended to be run periodically (a scheduled task / the auth:prune-remember-tokens command) since abandoned cookies are otherwise only cleaned up lazily on access.
func (*Remember) Resolve ¶
Resolve validates a remember cookie value and returns the user id plus, when the validator was rotated, a fresh cookie value the caller must set (empty means "leave the cookie unchanged"). Behaviour:
- current validator → rotate via an atomic compare-and-set and return the new cookie value. If a concurrent request rotated first (0 rows updated), authenticate anyway but issue no new cookie (the other response set it).
- the just-superseded validator within the grace window → accept (a concurrent request still carrying it), without rotating or revoking.
- anything else (unknown/expired/wrong/too-old) → ErrInvalidRememberToken; a known selector with a stale-beyond-grace validator is treated as theft and revokes the user's whole token family.
func (*Remember) Revoke ¶
Revoke deletes the token identified by the cookie value's selector (logout of this device). A malformed value or unknown selector is a no-op.
func (*Remember) RevokeAllForUser ¶
RevokeAllForUser deletes every remember token for a user (e.g. on a password change, to force re-authentication everywhere).
type SessionView ¶
type SessionView struct {
ID uuid.UUID
IP string
UserAgent string
CreatedAt time.Time
LastActiveAt time.Time
IsCurrent bool
}
SessionView is the service-level projection of a tracked session, with the current-session marker resolved (the secret session id is never exposed).
type Sessions ¶
type Sessions struct {
// contains filtered or unexported fields
}
Sessions manages the active-session list and termination. It tracks one row per login; deleting a row terminates that session (its next request is rejected by the TrackSession middleware).
func NewSessions ¶
func NewSessions(repo repositories.SessionRepository, activeWindow time.Duration) *Sessions
NewSessions builds the sessions service. activeWindow <= 0 falls back to DefaultSessionActiveWindow; pass the configured session lifetime so the list hides sessions whose store entry has already expired.
func (*Sessions) ForgetAllForUser ¶
ForgetAllForUser removes every session row for a user (e.g. password change).
func (*Sessions) List ¶
func (s *Sessions) List(ctx context.Context, userID uuid.UUID, currentSessionID string) ([]SessionView, error)
List returns the user's active sessions (most recent first), marking the one that matches currentSessionID.
func (*Sessions) PruneStale ¶
PruneStale deletes session rows whose store entry has certainly expired.
func (*Sessions) Terminate ¶
func (s *Sessions) Terminate(ctx context.Context, userID, publicID uuid.UUID, currentSessionID string) error
Terminate deletes one of the user's sessions by its public id. It refuses to terminate the current session (use logout) and returns ErrNotFound when the id is unknown or belongs to another user.
func (*Sessions) TerminateOthers ¶
func (s *Sessions) TerminateOthers(ctx context.Context, userID uuid.UUID, currentSessionID string) error
TerminateOthers deletes every session for the user except the current one.
type TwoFactor ¶
type TwoFactor struct {
// contains filtered or unexported fields
}
TwoFactor orchestrates TOTP enrollment, verification, and recovery codes. The secret and recovery codes are stored encrypted via the Crypter.
func NewTwoFactor ¶
func NewTwoFactor(repo repositories.UsersRepository, crypter Crypter, issuer string, recoveryCount int) *TwoFactor
NewTwoFactor builds the two-factor service. Empty issuer / non-positive recoveryCount fall back to the package defaults.
func (*TwoFactor) Confirm ¶
Confirm verifies a code against the pending secret, activates 2FA, and returns freshly generated recovery codes (shown to the user once).
func (*TwoFactor) ConsumeRecoveryCode ¶
func (s *TwoFactor) ConsumeRecoveryCode(ctx context.Context, id uuid.UUID, code string) (bool, error)
ConsumeRecoveryCode verifies a recovery code and marks it used (single-use). The read-marked-write is atomic under a row lock to prevent double-spend.
func (*TwoFactor) Enable ¶
Enable starts enrollment: it generates a TOTP secret, stores it encrypted (not yet confirmed), and returns the secret + otpauth URL for the user to scan. Re-enrolling a confirmed user returns ErrTwoFactorAlreadyEnabled.
func (*TwoFactor) RegenerateRecoveryCodes ¶
RegenerateRecoveryCodes replaces the recovery codes, invalidating the old set.
func (*TwoFactor) RemainingRecoveryCodes ¶
RemainingRecoveryCodes returns how many unused recovery codes the user has left. Plaintext codes are never returned after generation: they are stored as one-way hashes, so the only way to see them is once, at (re)generation time.
func (*TwoFactor) Verify ¶
Verify checks a TOTP code against a confirmed user's secret WITHOUT replay protection. Used by the programmatic facade; the login challenge uses VerifyLoginCode instead, which is single-use.
func (*TwoFactor) VerifyLoginCode ¶
VerifyLoginCode verifies a TOTP code for the login challenge and enforces single-use: the code's time-step must be newer than the last accepted one (rejecting replay within the validity window). The check + the last-used stamp are written atomically under a row lock.
type Users ¶
type Users struct {
// contains filtered or unexported fields
}
Users orchestrates admin user-management use cases and owns their validation.
func NewUsers ¶
func NewUsers(repo repositories.UsersRepository, hasher Hasher, minPwLen int, roles, managementRoles []string) *Users
NewUsers builds the user-management service. minPwLen <= 0 falls back to DefaultMinPasswordLength. roles, when non-empty, restricts the accepted role values (Create/Update reject anything outside the set); empty means any role. managementRoles is the set of privileged roles (those that can manage users); it backs the "default new users to non-admin" choice and the "keep at least one active admin" invariants on delete/disable/demote.
func (*Users) Create ¶
func (s *Users) Create(ctx context.Context, email, name, password, role string) (*models.User, error)
Create validates input and inserts a new user. Idempotent callers (e.g. a bootstrap command) should check for ErrAlreadyExists.
func (*Users) Delete ¶
Delete removes a user, refusing to remove the last active admin. The guard counts users holding a management role (not total rows): deleting a management user is refused when no OTHER active management user remains.
func (*Users) SetPassword ¶
func (s *Users) SetPassword(ctx context.Context, id uuid.UUID, newPassword string) (*models.User, error)
SetPassword resets a user's password (admin action). It bumps password_changed_at, which invalidates that user's existing sessions.
func (*Users) Update ¶
func (s *Users) Update(ctx context.Context, id uuid.UUID, email, name, role string, disabled *bool, actorID uuid.UUID) (*models.User, error)
Update changes a user's email, name, and role (not the password). disabled, when non-nil, locks (true) or unlocks (false) the account; nil leaves the lock state untouched. It enforces two invariants on a user who currently holds a management role: it cannot self-disable, and it cannot be disabled or demoted away from its management role if it is the last active admin (ErrLastAdmin).