auth

package
v1.1.17 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: MPL-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package auth provides the CMS's user accounts: password hashing, the Postgres-backed user store, roles, and login throttling.

Index

Constants

View Source
const ResetTTL = time.Hour

ResetTTL is how long a reset link works. Long enough to walk to another device and open an inbox; short enough that a link forwarded or left in an abandoned mailbox goes stale the same hour it arrived.

Variables

View Source
var (
	// ErrNotFound is returned when no user matches the query.
	ErrNotFound = errors.New("auth: user not found")
	// ErrDuplicateEmail is returned by Insert/Update when the email is taken.
	ErrDuplicateEmail = errors.New("auth: email already in use")
	// ErrInvalidCredentials is returned by Authenticate for a bad email or
	// password, or an inactive account. It deliberately does not say which.
	ErrInvalidCredentials = errors.New("auth: invalid credentials")
)
View Source
var ErrInvalidHash = errors.New("auth: stored password hash is malformed")

ErrInvalidHash is returned by VerifyPassword when the stored hash is neither a well-formed argon2id PHC string nor a usable bcrypt hash.

View Source
var ErrResetInvalid = errors.New("auth: reset token invalid or expired")

ErrResetInvalid is returned for a token that is unknown, expired, or already used. Deliberately one error for all three: distinguishing them would tell a guesser which failures were near-misses.

Functions

func GenerateTOTPSecret added in v0.9.0

func GenerateTOTPSecret() (string, error)

GenerateTOTPSecret returns a fresh base32 secret for enrolling an authenticator app: 160 bits, RFC 4226's recommended key size.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword derives an argon2id hash of password and returns it in PHC string format, e.g. $argon2id$v=19$m=65536,t=1,p=4$<salt>$<hash>. Every hash this CMS writes — new accounts, password changes, resets — is argon2id; bcrypt is verified but never issued.

func NeedsRehash added in v0.9.0

func NeedsRehash(hash string) bool

NeedsRehash reports whether a stored hash should be replaced the next time we hold the plaintext that goes with it — because it is bcrypt (an imported account we have not migrated yet) or argon2id at cost parameters we have since moved off.

A malformed hash also reports true, which costs nothing: the only caller rehashes after a successful verification, and nothing verifies against a hash we cannot parse.

func TOTPCode added in v0.9.0

func TOTPCode(secret string, t time.Time) (string, error)

TOTPCode returns the six-digit code for the secret at time t — what an authenticator app holding the same secret shows at that moment.

func TOTPProvisioningURI added in v0.9.0

func TOTPProvisioningURI(issuer, account, secret string) string

TOTPProvisioningURI builds the otpauth:// URL an authenticator app enrolls from (usually via a QR code): issuer and account label the entry in the app, secret is the shared key.

func ValidPermissionKey added in v0.9.0

func ValidPermissionKey(k string) bool

ValidPermissionKey reports whether k is acceptable as a permission name: a lowercase letter followed by up to 63 lowercase letters, digits, hyphens, or underscores.

func VerifyPassword

func VerifyPassword(password, hash string) (bool, error)

VerifyPassword reports whether password matches the stored hash, which may be either argon2id or bcrypt. Stored hashes are self-describing — argon2id PHC strings open with "$argon2id$", bcrypt's with "$2a$", "$2b$", or "$2y$" — so a site carrying accounts imported from a bcrypt system can verify both while it migrates. See NeedsRehash for the other half of that migration.

The argon2id comparison is constant time in the derived key; bcrypt's is constant time in the package.

func VerifyTOTP added in v0.9.0

func VerifyTOTP(secret, code string, t time.Time) (step int64, ok bool)

VerifyTOTP reports whether code matches the secret at time t, accepting one step of clock skew either side. On success it returns the step the code matched, which callers must claim (Store.ConsumeTOTPStep) so the same code cannot be accepted twice.

Types

type Permission added in v0.9.0

type Permission string

Permission names one grantable capability. The built-in permissions cover the CMS's own admin areas; a deployment may declare more (via cms.Config.Permissions or an admin section's Permission field) and check them in its own handlers with User.Can.

Permissions gate editor-role accounts only: the admin and superadmin roles implicitly hold every permission, built-in or custom.

const (
	// PermBlogs grants the blog feed: creating, editing, and publishing
	// blog posts, in the admin and the in-place editor.
	PermBlogs Permission = "blogs"
	// PermNews grants the news feed, the same way PermBlogs grants blog.
	PermNews Permission = "news"
	// PermPages grants site pages and everything that shapes them:
	// creating and editing pages, the navigation menus, and the
	// non-code site settings (name, logo, menu alignment) — all
	// through the in-place editor; the admin panel's Pages section
	// itself is superadmin-only.
	PermPages Permission = "pages"
	// PermUsers grants user management. A non-admin holder manages
	// editor accounts only: they cannot touch admin accounts, assign
	// admin roles, or grant permissions they do not hold themselves.
	PermUsers Permission = "users"
)

func BuiltinPermissions added in v0.9.0

func BuiltinPermissions() []Permission

BuiltinPermissions returns the permissions the CMS itself defines, in the order the user form lists them.

func PermissionForFeed added in v0.9.0

func PermissionForFeed(feed string) Permission

PermissionForFeed returns the permission governing a post feed name.

func PermissionForSlug added in v0.9.0

func PermissionForSlug(slug string) Permission

PermissionForSlug returns the permission that governs the page at slug. Post slugs always live under their feed ("blog/…", "news/…" — see content.Post), so the slug alone decides: those prefixes map to the feed permissions and every other slug is a site page.

type Role

type Role string

Role controls what a user may do in the admin area.

const (
	// RoleSuperadmin has every admin power plus snippet management, the
	// admin panel's Pages section, and unlisted page templates.
	RoleSuperadmin Role = "superadmin"
	// RoleAdmin may manage users and site settings in addition to content.
	RoleAdmin Role = "admin"
	// RoleEditor may create and edit content but not manage users.
	RoleEditor Role = "editor"
)

func (Role) IsAdmin

func (r Role) IsAdmin() bool

IsAdmin reports whether the role carries admin powers (user management, unsanitized content, page CSS/JS). Superadmin is a superset of admin.

func (Role) IsSuperadmin

func (r Role) IsSuperadmin() bool

IsSuperadmin reports whether the role carries the superadmin-only powers (snippet management, the Pages section, unlisted templates).

func (Role) Valid

func (r Role) Valid() bool

Valid reports whether r is a role the CMS knows about.

type Store

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

Store reads and writes users in Postgres.

func NewStore

func NewStore(db *sqldb.DB) *Store

NewStore returns a Store backed by db.

func (*Store) All

func (s *Store) All(ctx context.Context) ([]User, error)

All returns every user, ordered by name then email.

func (*Store) Authenticate

func (s *Store) Authenticate(ctx context.Context, email, password string) (*User, error)

Authenticate checks email and password and returns the matching active user, or ErrInvalidCredentials. To resist timing probes for valid addresses, it verifies a dummy hash when the email is unknown.

func (*Store) ConsumeReset added in v0.9.0

func (s *Store) ConsumeReset(ctx context.Context, token string) (*User, error)

ConsumeReset spends a live token: the row is deleted and the user it belonged to is returned, exactly once. A second call with the same token — a replayed link, a double submit — finds no row and gets ErrResetInvalid, which is the property that makes these single-use.

The delete is the claim. DELETE ... RETURNING would be the elegant spelling, but MySQL has no RETURNING; deleting by hash first and only then looking the user up would tell a racing duplicate that it lost, though not who won — and losing is the correct outcome for it.

func (*Store) ConsumeTOTPStep added in v0.9.0

func (s *Store) ConsumeTOTPStep(ctx context.Context, id int64, step int64) (bool, error)

ConsumeTOTPStep claims the step a verified code matched, exactly once. It reports false when the step was already claimed — a replayed code — which callers must treat as a failed login. The guard is the WHERE clause, so two racing submissions of one code resolve in the database: one wins, the other reads zero rows.

func (*Store) Count

func (s *Store) Count(ctx context.Context) (int, error)

Count returns the total number of users.

func (*Store) Delete added in v1.0.0

func (s *Store) Delete(ctx context.Context, id int64) error

Delete removes a user outright. The schema does the bookkeeping: grants and password-reset tokens are dropped with the row, while media uploads and posts survive with their user reference nulled. Returns ErrNotFound when no such user exists.

func (*Store) DisableTOTP added in v0.9.0

func (s *Store) DisableTOTP(ctx context.Context, id int64) error

DisableTOTP turns two-factor off for the user — their own choice on the settings page, or an admin rescuing somebody who lost their phone.

func (*Store) EnableTOTP added in v0.9.0

func (s *Store) EnableTOTP(ctx context.Context, id int64, secret string, confirmedStep int64) error

EnableTOTP stores a confirmed secret, turning two-factor on for the user. confirmedStep is the step of the code that proved the enrollment; recording it spends that code, so it cannot be replayed at the next login.

func (*Store) GetByEmail

func (s *Store) GetByEmail(ctx context.Context, email string) (*User, error)

GetByEmail returns the user with the given email (case-insensitive), grants included, or ErrNotFound.

func (*Store) GetByID

func (s *Store) GetByID(ctx context.Context, id int64) (*User, error)

GetByID returns the user with the given id, grants included, or ErrNotFound.

func (*Store) Insert

func (s *Store) Insert(ctx context.Context, u *User) (int64, error)

Insert stores a new user and returns its id. Email is normalized to lower case. Returns ErrDuplicateEmail if the address is taken.

func (*Store) MintReset added in v0.9.0

func (s *Store) MintReset(ctx context.Context, userID int64) (string, error)

MintReset creates a reset token for the user and returns the one usable copy of it. Any previous token the user held is revoked — asking twice leaves one working link, the newest — and expired rows are swept while we are here, so the table cannot accumulate.

func (*Store) ReplacePermissions added in v0.9.0

func (s *Store) ReplacePermissions(ctx context.Context, userID int64, perms []Permission) error

ReplacePermissions makes perms the user's exact set of grants, removing any not listed. Duplicates in perms are collapsed. The change is atomic: readers see the old set or the new one, never a half-written mix.

func (*Store) ResetUser added in v0.9.0

func (s *Store) ResetUser(ctx context.Context, token string) (*User, error)

ResetUser returns the user a live token belongs to, without spending it. This is the GET half of the flow — showing the new-password form — which must not consume anything, because rendering a form is not using it: the token has to survive until the form actually comes back.

func (*Store) SetLogger added in v0.9.0

func (s *Store) SetLogger(l *slog.Logger)

SetLogger directs the store's background reporting at l.

func (*Store) Update

func (s *Store) Update(ctx context.Context, u *User) error

Update saves email, name, role, and active for an existing user. It does not touch the password; use UpdatePassword for that.

func (*Store) UpdatePassword

func (s *Store) UpdatePassword(ctx context.Context, id int64, passwordHash string) error

UpdatePassword replaces a user's password hash.

type Throttle

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

Throttle is a small in-memory failed-login limiter. Keys are typically "email|remote-ip". It is per-process; that is sufficient to blunt online password guessing, which is all it aims to do.

func NewThrottle

func NewThrottle(limit int, window time.Duration) *Throttle

NewThrottle returns a Throttle allowing limit failures per key per window.

func (*Throttle) Blocked

func (t *Throttle) Blocked(key string) bool

Blocked reports whether key has exceeded its failure allowance.

func (*Throttle) Fail

func (t *Throttle) Fail(key string)

Fail records a failed attempt for key.

func (*Throttle) Reset

func (t *Throttle) Reset(key string)

Reset clears the failure count for key, e.g. after a successful login.

type User

type User struct {
	ID           int64
	Email        string
	Name         string
	PasswordHash string
	Role         Role
	Active       bool
	// Permissions are the user's grants, loaded by GetByID and
	// GetByEmail (All leaves it nil — the users list doesn't need
	// them). Meaningful for editors only; admin roles pass every
	// Can check regardless of what is stored here.
	Permissions []Permission
	// TOTPSecret is the base32 key an authenticator app was enrolled
	// with, empty when two-factor is off. TOTPLastStep is the time step
	// of the last accepted code; see Store.ConsumeTOTPStep.
	TOTPSecret   string
	TOTPLastStep int64
	CreatedAt    time.Time
	UpdatedAt    time.Time
}

User is a CMS account.

func (*User) Can added in v0.9.0

func (u *User) Can(p Permission) bool

Can reports whether the user holds the permission. Admin and superadmin roles hold every permission; editors hold what has been granted to them. Safe to call on a nil user (false).

func (*User) CanAny added in v0.9.0

func (u *User) CanAny(perms ...Permission) bool

CanAny reports whether the user holds at least one of the permissions.

func (*User) HasGrant added in v0.9.0

func (u *User) HasGrant(p Permission) bool

HasGrant reports whether the user holds the permission as an explicit grant — or is a superadmin, who holds everything, as always. This is the check behind capabilities that can be switched on and off per user whatever their role: unlike Can, the admin role earns nothing implicitly here. Safe to call on a nil user (false).

func (*User) TwoFactorEnabled added in v0.9.0

func (u *User) TwoFactorEnabled() bool

TwoFactorEnabled reports whether the user has finished enrolling an authenticator app. Enrollment is only saved once a live code has confirmed it, so a non-empty secret is the whole answer.

Jump to

Keyboard shortcuts

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