Documentation
¶
Overview ¶
Package auth provides the CMS's user accounts: password hashing, the Postgres-backed user store, roles, and login throttling.
Index ¶
- Constants
- Variables
- func GenerateTOTPSecret() (string, error)
- func HashPassword(password string) (string, error)
- func NeedsRehash(hash string) bool
- func TOTPCode(secret string, t time.Time) (string, error)
- func TOTPProvisioningURI(issuer, account, secret string) string
- func ValidPermissionKey(k string) bool
- func VerifyPassword(password, hash string) (bool, error)
- func VerifyTOTP(secret, code string, t time.Time) (step int64, ok bool)
- type Permission
- type Role
- type Store
- func (s *Store) All(ctx context.Context) ([]User, error)
- func (s *Store) Authenticate(ctx context.Context, email, password string) (*User, error)
- func (s *Store) ConsumeReset(ctx context.Context, token string) (*User, error)
- func (s *Store) ConsumeTOTPStep(ctx context.Context, id int64, step int64) (bool, error)
- func (s *Store) Count(ctx context.Context) (int, error)
- func (s *Store) Delete(ctx context.Context, id int64) error
- func (s *Store) DisableTOTP(ctx context.Context, id int64) error
- func (s *Store) EnableTOTP(ctx context.Context, id int64, secret string, confirmedStep int64) error
- func (s *Store) GetByEmail(ctx context.Context, email string) (*User, error)
- func (s *Store) GetByID(ctx context.Context, id int64) (*User, error)
- func (s *Store) Insert(ctx context.Context, u *User) (int64, error)
- func (s *Store) MintReset(ctx context.Context, userID int64) (string, error)
- func (s *Store) ReplacePermissions(ctx context.Context, userID int64, perms []Permission) error
- func (s *Store) ResetUser(ctx context.Context, token string) (*User, error)
- func (s *Store) SetLogger(l *slog.Logger)
- func (s *Store) Update(ctx context.Context, u *User) error
- func (s *Store) UpdatePassword(ctx context.Context, id int64, passwordHash string) error
- type Throttle
- type User
Constants ¶
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 ¶
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") )
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.
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
GenerateTOTPSecret returns a fresh base32 secret for enrolling an authenticator app: 160 bits, RFC 4226's recommended key size.
func HashPassword ¶
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
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
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
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
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 ¶
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
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 ¶
IsAdmin reports whether the role carries admin powers (user management, unsanitized content, page CSS/JS). Superadmin is a superset of admin.
func (Role) IsSuperadmin ¶
IsSuperadmin reports whether the role carries the superadmin-only powers (snippet management, the Pages section, unlisted templates).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes users in Postgres.
func (*Store) Authenticate ¶
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
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
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) Delete ¶ added in v1.0.0
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
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
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 ¶
GetByEmail returns the user with the given email (case-insensitive), grants included, or ErrNotFound.
func (*Store) GetByID ¶
GetByID returns the user with the given id, grants included, or ErrNotFound.
func (*Store) Insert ¶
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
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
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
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.
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 ¶
NewThrottle returns a Throttle allowing limit failures per key per window.
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
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.