auth

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 27 Imported by: 0

Documentation

Overview

Package auth keeps the accounts of the people who edit the site, their sessions and their groups of rights. Passwords are stored as Argon2id hashes, and sessions only as a SHA-256 digest of their token, so a copy of the database lets nobody sign in. Visitors of the public site have no accounts.

An account can turn on two-step sign-in: after the password, signing in asks for a code of an authenticator app (TOTP) or a recovery code (see SignIn). The secrets the apps share are stored encrypted with a key of the site (see UseSecrets), recovery codes only as digests.

Index

Constants

View Source
const (
	// Read opens a section and its items.
	Read = "read"
	// Create adds new items.
	Create = "create"
	// Edit changes items.
	Edit = "edit"
	// Publish makes changes visible on the public site, or hides them.
	Publish = "publish"
	// Delete moves items to the trash, and brings them back or deletes
	// them for good there.
	Delete = "delete"
	// Review approves or rejects revisions submitted for review, in sections
	// whose review workflow is on (see content.Review).
	Review = "review"
	// Export downloads the personal data a section holds, such as a
	// newsletter's subscribers or the registrations of an event, as CSV.
	Export = "export"
)

Actions in the permission matrix, the columns a group's rights are set in for every section.

View Source
const CookieName = "wt_admin"

CookieName is the admin session cookie.

View Source
const PendingCookieName = "wt_admin_pending"

PendingCookieName is the cookie of a pending sign-in, which waits for the code of its second step (see SignIn).

View Source
const PendingLifetime = 5 * time.Minute

PendingLifetime is how long a sign-in whose password was right waits for the code of its second step (see SignIn).

View Source
const (
	// ResetLifetime is how long a password reset link works.
	ResetLifetime = time.Hour
)

Variables

View Source
var (
	// ErrCredentials is a sign-in with an unknown e-mail address or a wrong
	// password. The two are not told apart, so the answer reveals no
	// accounts.
	ErrCredentials = errors.New("invalid_credentials")
	// ErrSession is a session token that is unknown, expired, revoked or
	// belongs to a disabled account.
	ErrSession = errors.New("invalid_session")
	// ErrBootstrap refuses the first-run setup once any account exists.
	ErrBootstrap = errors.New("bootstrap_requires_empty_accounts")
	// ErrPassword is a password shorter than 12 characters (or absurdly
	// long).
	ErrPassword = errors.New("password_too_short")
	// ErrEmail is an e-mail address that is not one.
	ErrEmail = errors.New("invalid_email")
	// ErrDuplicate is an e-mail address another account has, or a group
	// name another group has.
	ErrDuplicate = errors.New("duplicate_email_or_group")
	// ErrRateLimit refuses a sign-in after too many failed attempts for the
	// same address in a short time, to slow down guessing.
	ErrRateLimit = errors.New("login_rate_limited")
	// ErrNotFound is an account, session or group that does not exist.
	ErrNotFound = errors.New("account_not_found")
	// ErrLastAdmin refuses a change that would leave no active
	// administrator, as nobody could manage the accounts any more.
	ErrLastAdmin = errors.New("last_active_admin")
)
View Source
var (
	// ErrSecondStep is the correct password of an account with two-step
	// sign-in on, given to Login. Such accounts sign in with SignIn and
	// FinishSignIn.
	ErrSecondStep = errors.New("second_step_required")
	// ErrCode is a code that is wrong or was used already: a code of the
	// authenticator app or a recovery code.
	ErrCode = errors.New("invalid_code")
	// ErrPending is a pending sign-in that is unknown, finished or older
	// than PendingLifetime.
	ErrPending = errors.New("invalid_pending_sign_in")
	// ErrTwoStepOn refuses to set up two-step sign-in while it is on.
	ErrTwoStepOn = errors.New("two_step_on")
	// ErrTwoStepOff is an account without two-step sign-in, or without a
	// setup that waits for its first code.
	ErrTwoStepOff = errors.New("two_step_off")
	// ErrNoSecrets refuses two-step sign-in on a service without the site's
	// keys (see UseSecrets): it could not keep the secrets of
	// authenticator apps.
	ErrNoSecrets = errors.New("two_step_without_keys")
)

Actions lists the matrix columns in order.

View Source
var ErrGroupName = errors.New("invalid_group_name")

ErrGroupName is a group name that is empty or longer than 100 characters.

View Source
var ErrResetToken = errors.New("invalid_reset_token")

ErrResetToken is returned for a password reset link that is unknown, used or expired.

Functions

func ClearCookie

func ClearCookie(w http.ResponseWriter, secure bool)

ClearCookie removes the session cookie.

func ClearPendingCookie

func ClearPendingCookie(w http.ResponseWriter, secure bool)

ClearPendingCookie removes the cookie of a pending sign-in.

func KeyURI

func KeyURI(issuer, account string, secret []byte) string

KeyURI is the otpauth address a QR code gives an authenticator app (the Key URI Format of Google Authenticator, which other apps read too): the issuer and account name the app shows, the secret, and the settings of TOTP. issuer is the site, such as its title; account the e-mail address.

func PendingToken

func PendingToken(r *http.Request) string

PendingToken reads the token of a pending sign-in from the request cookie.

func Schema

func Schema() fs.FS

Schema holds this package's schema files per dialect (see db.ApplySchema).

func SecretText

func SecretText(secret []byte) string

SecretText is secret as people type it into an authenticator app: base32 (RFC 4648) without padding, such as GEZDGNBVGY3TQOJQ.

func SetCookie

func SetCookie(w http.ResponseWriter, token string, secure bool)

SetCookie stores the session token. secure must be false only for plain HTTP. The cookie reaches the whole site, so public pages can show editors an edit link, wherever the admin is; it stays HttpOnly and SameSite=Lax, and the admin refuses cross-origin form posts (see http.CrossOriginProtection).

func SetPendingCookie

func SetPendingCookie(w http.ResponseWriter, token string, secure bool)

SetPendingCookie stores the token of a pending sign-in for PendingLifetime. secure must be false only for plain HTTP. The cookie is HttpOnly and SameSite=Strict: only the admin's own page for the code sends it back.

func TOTP

func TOTP(secret []byte, at time.Time) string

TOTP returns the code an authenticator app shows for secret at time at (RFC 6238), with the settings every app uses: the HOTP value (RFC 4226, HMAC-SHA1) of the number of 30-second steps since the Unix epoch, as 6 digits.

func Token

func Token(r *http.Request) string

Token reads the session token from the request cookie.

func WithUser

func WithUser(ctx context.Context, user User) context.Context

WithUser returns ctx signed in as user. Stores record the user as the actor of their changes (see db.WithActor).

Types

type Attempt

type Attempt struct {
	// Session is the token of the new session.
	Session string
	// Pending is the token of the pending sign-in, for the browser to keep
	// (see SetPendingCookie) until FinishSignIn turns it into a session,
	// within PendingLifetime.
	Pending string
}

Attempt is what SignIn gives for a correct password: a session, or for an account with two-step sign-in on, a pending sign-in that waits for a code. One of the two is set.

type Group

type Group struct {
	ID          string      `db:"id"`
	Name        string      `db:"name"`
	CreatedAt   time.Time   `db:"created_at"`
	CreatedBy   db.Ref      `db:"created_by"`
	UpdatedAt   time.Time   `db:"updated_at"`
	UpdatedBy   db.Ref      `db:"updated_by"`
	Members     []string    `db:"-"`
	Permissions Permissions `db:"-"`
}

Group gives its members rights: allowed actions per section (Pages, Media, a module instance, ...). Administrators do not need groups.

type Permissions

type Permissions map[string][]string

Permissions maps a section ID to its allowed actions.

func (Permissions) Allows

func (p Permissions) Allows(section, action string) bool

Allows reports whether action is allowed in section.

type Role

type Role string

Role is what an account may do in general. Administrators may do everything; editors may do what their groups allow.

const (
	// Admin may do everything, including managing accounts and groups.
	Admin Role = "admin"
	// Editor may do what the account's groups allow, and nothing else.
	Editor Role = "editor"
)

type Secrets

type Secrets interface {
	Secret(ctx context.Context, name string) ([]byte, error)
}

Secrets hands out the site's keys: random keys of 32 bytes by name, the same in every process of the site, made on first use. site.Service is one (site.Service.Secret).

type Service

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

Service manages accounts, sessions and groups in the database. It is safe for concurrent use.

func New

func New(database *db.DB) (*Service, error)

New returns the Service on database, whose schema (Schema) must be applied. It fails only when the system has no randomness.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, token string) (User, error)

Authenticate returns the active user of a valid session token and records the session's activity, to the minute.

func (*Service) BeginTwoStep

func (s *Service) BeginTwoStep(ctx context.Context, userID string) error

BeginTwoStep starts turning on two-step sign-in for the account: it makes a random secret of 20 bytes, which SetupSecret returns for the authenticator app, in place of the secret of an earlier setup. Two-step sign-in is on only once ConfirmTwoStep receives a code of the app. It returns ErrTwoStepOn while it is on already.

func (*Service) Bootstrap

func (s *Service) Bootstrap(ctx context.Context, email, password string) error

Bootstrap creates the first admin. It does nothing once any account exists and returns ErrBootstrap in that case.

func (*Service) CancelTwoStep

func (s *Service) CancelTwoStep(ctx context.Context, userID string) error

CancelTwoStep drops the account's setup that waits for its first code, if any. It does not turn off two-step sign-in that is on.

func (*Service) ChangePassword

func (s *Service) ChangePassword(ctx context.Context, userID, current, password, keep string) error

ChangePassword sets a new password after checking the current one. Wrong guesses count towards the same limit as failed sign-ins. Every other session of the account is revoked, and its pending sign-ins end; the session with token keep, the one the change was made in, stays signed in.

func (*Service) CheckPending

func (s *Service) CheckPending(ctx context.Context, pending string) error

CheckPending reports whether a pending sign-in still waits for its code: ErrPending when it is unknown, finished or expired.

func (*Service) CheckResetToken

func (s *Service) CheckResetToken(ctx context.Context, token string) error

CheckResetToken reports whether a reset token can still be used, so the form to choose a new password is only shown for working links.

func (*Service) Cleanup

func (s *Service) Cleanup(ctx context.Context) error

Cleanup removes expired sessions and pending sign-ins, ended login windows and expired password reset links. The app runs it in the background.

func (*Service) ConfirmTwoStep

func (s *Service) ConfirmTwoStep(ctx context.Context, userID, code string) ([]string, error)

ConfirmTwoStep turns two-step sign-in on with a code of the app the setup was scanned into, and returns the account's 10 recovery codes, such as 4f7k2-9xq3m. Only their digests are stored, so they cannot be shown again. A wrong code returns ErrCode and leaves the setup waiting; without a setup it returns ErrTwoStepOff.

func (*Service) CreateUser

func (s *Service) CreateUser(ctx context.Context, email, name, password string, role Role) (User, error)

CreateUser adds an active account. Passwords need at least 12 characters.

func (*Service) DeleteGroup

func (s *Service) DeleteGroup(ctx context.Context, id string) error

DeleteGroup removes a group; its members keep their other groups.

func (*Service) DeleteUser

func (s *Service) DeleteUser(ctx context.Context, userID string) error

DeleteUser removes an account with its sessions, group memberships, reset links and two-step sign-in. The last active administrator cannot be removed.

func (*Service) DisableTwoStep

func (s *Service) DisableTwoStep(ctx context.Context, userID, password, code string) error

DisableTwoStep turns two-step sign-in off with the account's password and a code: of the app or a recovery code. Wrong passwords and codes count towards the sign-in limit (ErrRateLimit). It returns ErrTwoStepOff when it is off already.

func (*Service) FinishSignIn

func (s *Service) FinishSignIn(ctx context.Context, pending, code, previous string) (string, error)

FinishSignIn completes a pending sign-in (see SignIn) with a code of the authenticator app or a recovery code, and returns a new session token. A previous token from the same browser is revoked. A code of the app works for its 30-second step and one step either side, and only once: a step that was used, or one before it, is refused. Each recovery code works once. Wrong codes return ErrCode and count towards the sign-in limit (ErrRateLimit), together with wrong passwords; the pending sign-in stays until it expires.

func (*Service) Group

func (s *Service) Group(ctx context.Context, id string) (Group, error)

Group returns one group with its members and permissions.

func (*Service) Groups

func (s *Service) Groups(ctx context.Context) ([]Group, error)

Groups lists all groups by name, with members and permissions.

func (*Service) HasUsers

func (s *Service) HasUsers(ctx context.Context) (bool, error)

HasUsers reports whether any account exists. Without one, the admin offers its first-run setup.

func (*Service) Login

func (s *Service) Login(ctx context.Context, email, password, previous string) (string, error)

Login checks credentials and returns a new session token. A previous token from the same browser is revoked. Errors never reveal whether an account exists. An account with two-step sign-in on gets no session from its password alone: Login returns ErrSecondStep, and such an account signs in with SignIn.

func (*Service) Logout

func (s *Service) Logout(ctx context.Context, token string) error

Logout ends the session of token, so it no longer signs anyone in. An empty or unknown token is no error: there is nothing to end.

func (*Service) Names

func (s *Service) Names(ctx context.Context) (map[string]string, error)

Names maps user IDs to display names, for revision history.

func (*Service) NewRecoveryCodes

func (s *Service) NewRecoveryCodes(ctx context.Context, userID, code string) ([]string, error)

NewRecoveryCodes replaces the account's recovery codes with 10 new ones, with a code: of the app or one of the old recovery codes. Wrong codes count towards the sign-in limit (ErrRateLimit).

func (*Service) Peek

func (s *Service) Peek(ctx context.Context, token string) (User, error)

Peek is Authenticate without recording activity: one read, for checks on the public site where editors only browse.

func (*Service) Permissions

func (s *Service) Permissions(ctx context.Context, user User) (Permissions, error)

Permissions returns what a user may do: the combined rights of their groups. Administrators may do everything and get nil; check IsAdmin first.

func (*Service) RemoveTwoStep

func (s *Service) RemoveTwoStep(ctx context.Context, userID string) error

RemoveTwoStep turns two-step sign-in off for an account without its password or a code, as an administrator does for someone who lost both their phone and their recovery codes. The account's pending sign-ins end. It returns ErrTwoStepOff when it is off already.

func (*Service) RequestPasswordReset

func (s *Service) RequestPasswordReset(ctx context.Context, email string) (string, User, error)

RequestPasswordReset creates a one-time token for the active account with email, to send in a reset link; only its digest is stored. It returns ErrNotFound when there is no such active account and ErrRateLimit after three links within an hour. Callers must answer the same in every case, so the answer does not reveal which addresses have an account.

func (*Service) ResetPassword

func (s *Service) ResetPassword(ctx context.Context, token, password string) error

ResetPassword sets a new password with a reset token and uses the token up, with every other reset link of the account. All sessions of the account are revoked, its pending sign-ins end and its failed sign-ins are forgotten, so it signs in anew with the new password: with two-step sign-in on, also with a code (see SignIn).

func (*Service) RevokeSession

func (s *Service) RevokeSession(ctx context.Context, userID, sessionID string) error

RevokeSession ends one session of an account, such as one on a lost phone. It returns ErrNotFound when the account has no such active session.

func (*Service) SaveGroup

func (s *Service) SaveGroup(ctx context.Context, group Group) (Group, error)

SaveGroup creates a group (empty ID) or renames it and replaces its permissions.

func (*Service) Sessions

func (s *Service) Sessions(ctx context.Context, userID string) ([]Session, error)

Sessions lists the account's sessions that have not expired yet, newest first; revoked ones are included until they would have expired.

func (*Service) SetRole

func (s *Service) SetRole(ctx context.Context, userID string, role Role) error

SetRole makes a user an administrator or an editor. The last active administrator cannot become an editor.

func (*Service) SetStatus

func (s *Service) SetStatus(ctx context.Context, userID string, status Status) error

SetStatus enables or disables an account. Disabling revokes its sessions; the last active admin cannot be disabled.

func (*Service) SetUserGroups

func (s *Service) SetUserGroups(ctx context.Context, userID string, groupIDs []string) error

SetUserGroups replaces the groups a user belongs to.

func (*Service) SetupAdmin

func (s *Service) SetupAdmin(ctx context.Context, email, name, password string) (User, error)

SetupAdmin creates the first account, an administrator, as Bootstrap does, with a name. It returns ErrBootstrap once any account exists, also when another request created one first.

func (*Service) SetupSecret

func (s *Service) SetupSecret(ctx context.Context, userID string) ([]byte, error)

SetupSecret returns the secret of the account's setup that waits for its first code (see BeginTwoStep), to show as a QR code (KeyURI) and as text (SecretText). Once two-step sign-in is on, the secret is never shown again: SetupSecret then returns ErrTwoStepOff, as it does without a setup.

func (*Service) SignIn

func (s *Service) SignIn(ctx context.Context, email, password, previous string) (Attempt, error)

SignIn checks credentials as Login does. For an account with two-step sign-in on, a correct password starts a pending sign-in rather than a session: FinishSignIn completes it with a code within PendingLifetime. Wrong passwords count towards the sign-in limit, and so do wrong codes; failures are forgotten only once a sign-in completes.

func (*Service) TwoStep

func (s *Service) TwoStep(ctx context.Context, userID string) (TwoStep, error)

TwoStep returns the state of the account's two-step sign-in.

func (*Service) TwoStepAvailable

func (s *Service) TwoStepAvailable() bool

TwoStepAvailable reports whether two-step sign-in can be turned on: the service has the site's keys (see UseSecrets).

func (*Service) UpdateUser

func (s *Service) UpdateUser(ctx context.Context, userID, email, name string) error

UpdateUser changes an account's e-mail address and name. The address must be valid and not in use by another account.

func (*Service) UseSecrets

func (s *Service) UseSecrets(secrets Secrets)

UseSecrets gives the service the site's keys. Two-step sign-in encrypts the secrets of authenticator apps with one of them (AES-GCM), named "two_step_sign_in"; without keys it cannot be turned on (ErrNoSecrets). Call it before the service is used.

func (*Service) User

func (s *Service) User(ctx context.Context, userID string) (User, error)

User returns one account, without its password hash.

func (*Service) UserGroups

func (s *Service) UserGroups(ctx context.Context, userID string) ([]string, error)

UserGroups lists the IDs of the groups a user belongs to.

func (*Service) Users

func (s *Service) Users(ctx context.Context) ([]User, error)

Users lists all accounts by e-mail address, without password hashes.

type Session

type Session struct {
	ID         string     `db:"id"`
	UserID     string     `db:"user_id"`
	ExpiresAt  time.Time  `db:"expires_at"`
	RevokedAt  *time.Time `db:"revoked_at"`
	CreatedAt  time.Time  `db:"created_at"`
	LastSeenAt *time.Time `db:"last_seen_at"`
}

Session is one sign-in of an account, on one browser, as the account's session list shows it. The token itself is never stored.

type Status

type Status string

Status tells whether an account may sign in.

const (
	// Active accounts may sign in.
	Active Status = "active"
	// Disabled accounts are kept, with their name on what they changed,
	// but may not sign in; disabling one ends its sessions.
	Disabled Status = "disabled"
)

type TwoStep

type TwoStep struct {
	// On means that signing in asks for a code after the password.
	On bool
	// Setup means that turning it on waits for the first code of the app
	// (see BeginTwoStep).
	Setup bool
	// RecoveryCodes is the number of recovery codes not used yet.
	RecoveryCodes int
}

TwoStep is the state of an account's two-step sign-in.

type User

type User struct {
	ID           string    `db:"id"`
	Email        string    `db:"email"`
	Name         string    `db:"name"`
	Role         Role      `db:"role"`
	Status       Status    `db:"status"`
	PasswordHash string    `db:"password_hash" json:"-"`
	CreatedAt    time.Time `db:"created_at"`
	CreatedBy    db.Ref    `db:"created_by"`
	UpdatedAt    time.Time `db:"updated_at"`
	UpdatedBy    db.Ref    `db:"updated_by"`
}

User is an account. Lists and the signed-in user (CurrentUser) carry no password hash; only sign-in reads it.

func CurrentUser

func CurrentUser(ctx context.Context) (User, bool)

CurrentUser returns the user that ctx is signed in as (see WithUser), and false outside a signed-in request, such as on the public site.

func (User) DisplayName

func (u User) DisplayName() string

DisplayName is the name, or the email when no name is set.

func (User) IsAdmin

func (u User) IsAdmin() bool

IsAdmin reports whether the user may do everything, including managing accounts and groups.

Jump to

Keyboard shortcuts

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