# modules/auth

```go
import "apistock.dev/modules/auth"
```

Package auth provides the building blocks of email and password authentication: argon2id password hashing, session tokens and one-time codes stored only as hashes, email normalisation, session cookies, the request middleware, a permission catalog and plain authentication emails (ADR-0024, ADR-0038).

Apps own their authentication flows, tables and SQL in internal/modules/auth (generated by aps new): its use cases call these helpers for the security-sensitive steps, so fixes to them arrive with go get, while every flow stays readable and editable in the app.

Stability: pre-1.0 (ADR-0015).

## Constants

### const DefaultSessionIdleTTL, …

```go
const (
	DefaultSessionIdleTTL          = 14 * 24 * time.Hour
	DefaultSessionAbsoluteTTL      = 90 * 24 * time.Hour
	DefaultVerificationCodeTTL     = 15 * time.Minute
	DefaultResetCodeTTL            = 30 * time.Minute
	DefaultDeletedAccountRetention = 30 * 24 * time.Hour

	// DefaultLoginAttempts per email address within DefaultLoginWindow.
	DefaultLoginAttempts = 10
	DefaultLoginWindow   = 15 * time.Minute

	MinPasswordLength = 12
	MaxPasswordLength = 128

	// CodeMaxAttempts is how many guesses one code allows.
	CodeMaxAttempts = 5
	// CodeResendInterval is the shortest time between two codes of the same
	// purpose for one account.
	CodeResendInterval = time.Minute
	// SessionTouchInterval is the shortest time between two last-seen updates
	// of a session, so busy clients don't write on every request.
	SessionTouchInterval = time.Minute
	// EndedSessionRetention is how long ended sessions stay for
	// investigations before cleanup removes them.
	EndedSessionRetention = 7 * 24 * time.Hour
	// CodeRetention is how long codes stay before cleanup removes them.
	CodeRetention = 24 * time.Hour

	DefaultCookieName = "__Host-session"

	// MFAChallengeTTL is how long a sign-in waits for its second factor.
	MFAChallengeTTL = 5 * time.Minute
	// MFAChallengeMaxAttempts is how many wrong second factors one sign-in
	// allows.
	MFAChallengeMaxAttempts = 5
)
```

Defaults and policy constants shared by generated apps.

### const TOTPDigits, …

```go
const (
	TOTPDigits = 6
	TOTPPeriod = 30 * time.Second
	// TOTPSkew is how many steps before and after the current one are
	// accepted, for clocks that drift.
	TOTPSkew = 1
)
```

TOTP parameters every authenticator app supports (RFC 6238).

### const RecentVerification

```go
const RecentVerification = 10 * time.Minute
```

RecentVerification is how long after verifying a second factor a session can change the account's sign-in methods without the password (ADR-0044), and how long after signing in an account without a password can confirm sensitive changes (ADR-0046).

### const RecoveryCodeCount

```go
const RecoveryCodeCount = 10
```

RecoveryCodeCount is how many recovery codes a user gets at a time.

## Variables

### var SessionIdleLimits, …

```go
var (
	SessionIdleLimits      = Limits{Min: 5 * time.Minute, Max: 90 * 24 * time.Hour}
	SessionAbsoluteLimits  = Limits{Min: time.Hour, Max: 365 * 24 * time.Hour}
	VerificationCodeLimits = Limits{Min: time.Minute, Max: time.Hour}
	ResetCodeLimits        = Limits{Min: time.Minute, Max: 2 * time.Hour}
	DeletedRetentionLimits = Limits{Min: 0, Max: 365 * 24 * time.Hour}
)
```

Hard limits for authentication durations.

### var ErrInvalidEmail, …

```go
var (
	// ErrInvalidEmail reports an email address that isn't valid.
	ErrInvalidEmail = errors.New("auth: invalid email address")

	// ErrWeakPassword reports a password that fails the password policy. The
	// error is a [*PasswordError].
	ErrWeakPassword = errors.New("auth: password does not meet the policy")

	// ErrUnauthenticated reports a missing, invalid, expired or revoked
	// session.
	ErrUnauthenticated = errors.New("auth: authentication required")
)
```

Errors returned by the helpers. Check them with [errors.Is](https://pkg.go.dev/errors#Is).

### var ErrInvalidKeyring, …

```go
var (
	// ErrInvalidKeyring reports an AUTH_ENCRYPTION_KEYS value that can't be
	// parsed.
	ErrInvalidKeyring = errors.New("auth: invalid encryption keys")
	// ErrUnknownKey reports a ciphertext encrypted with a key the keyring
	// doesn't hold.
	ErrUnknownKey = errors.New("auth: ciphertext was encrypted with an unknown key")
	// ErrDecrypt reports a ciphertext that was changed, or bound to other
	// additional data.
	ErrDecrypt = errors.New("auth: ciphertext can't be decrypted")
)
```

Errors returned by [Keyring](#Keyring).

### var ErrInvalidTOTPSecret

```go
var ErrInvalidTOTPSecret = errors.New("auth: invalid TOTP secret")
```

ErrInvalidTOTPSecret reports a secret that isn't base32.

## Functions

### func ClearSessionCookie

```go
func ClearSessionCookie(name string) *http.Cookie
```

ClearSessionCookie returns a cookie that removes the session cookie.

### func CodeMatches

```go
func CodeMatches(id, code string, hash []byte) bool
```

CodeMatches reports, in constant time, whether code is the one hashed as hash for row id.

### func HashCode

```go
func HashCode(id, code string) []byte
```

HashCode returns the hash to store for code, bound to its row ID so equal codes hash differently.

### func HashRecoveryCode

```go
func HashRecoveryCode(userID, code string) []byte
```

HashRecoveryCode returns the hash to store for a user's recovery code. Codes carry 50 random bits and are single-use, so a fast hash suffices; binding it to the user makes equal codes of two users hash differently.

### func HashToken

```go
func HashToken(token string) []byte
```

HashToken returns the SHA-256 of a token, for storing and looking it up.

### func Middleware

```go
func Middleware(a Authenticator, opts ...MiddlewareOption) func(http.Handler) http.Handler
```

Middleware stores every request's [ClientInfo](#ClientInfo) in its context, and authenticates requests carrying a session token in an "Authorization: Bearer" header or the session cookie, putting the principal and its actor in the context. Requests without a valid token continue anonymously: use cases decide what needs authentication. When the authenticator fails for another reason (the database is down) it responds 503, rather than treating a signed-in user as anonymous.

Cookie-authenticated requests need cross-origin protection (httpx.CrossOrigin) earlier in the chain.

### func NewCode

```go
func NewCode() string
```

NewCode returns a uniformly random 6-digit code.

### func NewID

```go
func NewID(prefix string) string
```

NewID returns a random identifier such as usr\_2x7…, with 128 bits of randomness.

### func NewKeyringKey

```go
func NewKeyringKey(id string) string
```

NewKeyringKey returns a new random key entry "id:base64key", for generating AUTH\_ENCRYPTION\_KEYS.

### func NewRecoveryCodes

```go
func NewRecoveryCodes() []string
```

NewRecoveryCodes returns [RecoveryCodeCount](#RecoveryCodeCount) single-use recovery codes, each 10 base32 characters (50 random bits) shown as "xxxxx-xxxxx". Show them once and store only [HashRecoveryCode](#HashRecoveryCode).

### func NewTOTPSecret

```go
func NewTOTPSecret() string
```

NewTOTPSecret returns a new TOTP secret: 20 random bytes (160 bits, as RFC 4226 recommends) in unpadded base32, the form authenticator apps accept when typed or scanned. Store it only encrypted ([Keyring](#Keyring)).

### func NewToken

```go
func NewToken() (token string, hash []byte)
```

NewToken returns a session token with 256 bits of randomness and the hash to store. Store only the hash.

### func NormalizeEmail

```go
func NormalizeEmail(s string) (email, normalized string, err error)
```

NormalizeEmail returns the address to send to and the lowercased address accounts are unique by, or [ErrInvalidEmail](#ErrInvalidEmail).

### func NormalizeRecoveryCode

```go
func NormalizeRecoveryCode(code string) string
```

NormalizeRecoveryCode returns code lowercased without spaces or hyphens, so "ABCDE-FGHIJ", "abcde fghij" and "abcdefghij" match.

### func SessionCookie

```go
func SessionCookie(name, token string, expires time.Time) *http.Cookie
```

SessionCookie returns the cookie that carries token for browsers: Secure, HttpOnly, SameSite=Lax, path "/", expiring at expires.

### func TOTPCode

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

TOTPCode returns the code for secret at t.

### func TOTPQRCode

```go
func TOTPQRCode(uri string) (string, error)
```

TOTPQRCode returns a PNG image of uri (from [TOTPURI](#TOTPURI)) as a data URL, ready for an \<img> tag, so an authenticator app can scan it before a client renders its own (ADR-0043).

### func TOTPStep

```go
func TOTPStep(t time.Time) int64
```

TOTPStep returns the time step containing t.

### func TOTPURI

```go
func TOTPURI(issuer, account, secret string) string
```

TOTPURI returns the otpauth:// URI authenticator apps read from a QR code: issuer is the app's name and account the user's email address.

### func TokenFrom

```go
func TokenFrom(r *http.Request, cookieName string) string
```

TokenFrom returns the request's bearer token, or else its session cookie.

### func ValidatePassword

```go
func ValidatePassword(ctx context.Context, password string, checker PasswordChecker) error
```

ValidatePassword applies the password policy to a new password: 12 to 128 characters, not blank, and checker when not nil. It returns a [\*PasswordError](#PasswordError).

### func VerifyTOTP

```go
func VerifyTOTP(secret, code string, now time.Time) (step int64, ok bool)
```

VerifyTOTP reports whether code is valid for secret at now, within [TOTPSkew](#TOTPSkew) steps, and returns the matching step. Store the step and accept a later code only for a later step, so a code can't be used twice. Spaces in code are ignored.

### func WithClientInfo

```go
func WithClientInfo(ctx context.Context, c ClientInfo) context.Context
```

WithClientInfo returns a copy of ctx carrying c.

### func WithPrincipal

```go
func WithPrincipal(ctx context.Context, p Principal) context.Context
```

WithPrincipal returns a copy of ctx carrying p and its actor.

## Types

### type Authenticator

```go
type Authenticator interface {
	Authenticate(ctx context.Context, token string) (Principal, error)
}
```

An Authenticator resolves a session token, returning [ErrUnauthenticated](#ErrUnauthenticated) for an unknown, ended or expired session. The app's auth use cases implement it.

### type Catalog

```go
type Catalog struct {
	// contains filtered or unexported fields
}
```

A Catalog declares the permissions modules check and the roles that grant them. Access is denied by default: a role grants only the permissions declared for it, and a role name stored for a user but missing from the catalog grants nothing. Declare everything at startup, then call [Catalog.Freeze](#Catalog.Freeze); invalid or late declarations are programming errors and panic.

#### func NewCatalog

```go
func NewCatalog() *Catalog
```

NewCatalog returns an empty catalog.

#### func (*Catalog) AllPermissions

```go
func (c *Catalog) AllPermissions() []Permission
```

AllPermissions returns every declared permission in declaration order.

#### func (*Catalog) Freeze

```go
func (c *Catalog) Freeze()
```

Freeze stops further declarations. Services using the catalog call it.

#### func (*Catalog) HasRole

```go
func (c *Catalog) HasRole(role string) bool
```

HasRole reports whether role is declared.

#### func (*Catalog) Permission

```go
func (c *Catalog) Permission(name, description string)
```

Permission declares a permission, such as "ops.settings.write".

#### func (*Catalog) Permissions

```go
func (c *Catalog) Permissions(roles ...string) []string
```

Permissions returns the sorted permissions granted by roles. Unknown roles grant nothing.

#### func (*Catalog) PermissionsFor

```go
func (c *Catalog) PermissionsFor(roles []string, mfaVerified bool) (granted, stepUp []string)
```

PermissionsFor returns the sorted permissions roles grant a session: granted holds those of roles that don't require two-factor authentication, plus those of roles that do when mfaVerified; stepUp holds the permissions only a verified session would add. Unknown roles grant nothing.

#### func (*Catalog) RequireMFA

```go
func (c *Catalog) RequireMFA(roles ...string)
```

RequireMFA marks declared roles as requiring two-factor authentication: their permissions are granted only to sessions verified with a second factor (ADR-0043). It is code, not a runtime setting, so operators can't weaken it.

#### func (*Catalog) RequiresMFA

```go
func (c *Catalog) RequiresMFA(roles ...string) bool
```

RequiresMFA reports whether any of roles requires two-factor authentication.

#### func (*Catalog) Role

```go
func (c *Catalog) Role(name, description string, permissions ...string)
```

Role declares a role granting permissions, each already declared.

#### func (*Catalog) Roles

```go
func (c *Catalog) Roles() []Role
```

Roles returns every declared role in declaration order.

### type ClientInfo

```go
type ClientInfo struct {
	IP        string
	UserAgent string
}
```

ClientInfo describes the client making a request, for sessions and audit events.

#### func ClientInfoFrom

```go
func ClientInfoFrom(r *http.Request) ClientInfo
```

ClientInfoFrom returns the client's IP address (from RemoteAddr: run trusted-proxy middleware first behind a proxy) and user agent.

#### func ClientInfoFromContext

```go
func ClientInfoFromContext(ctx context.Context) ClientInfo
```

ClientInfoFromContext returns the client details [Middleware](#Middleware) stored for the request, or empty details.

#### func (ClientInfo) Clean

```go
func (c ClientInfo) Clean() ClientInfo
```

Clean returns c with the IP address in canonical form (empty when it isn't one) and a bounded, valid user agent, ready to store.

### type Emails

```go
type Emails interface {
	// SendVerificationCode sends the code that verifies to's address.
	SendVerificationCode(ctx context.Context, to, code string, ttl time.Duration) error
	// SendPasswordResetCode sends the code that resets to's password.
	SendPasswordResetCode(ctx context.Context, to, code string, ttl time.Duration) error
	// SendAccountExists tells to that someone tried to register with an
	// address that already has an account.
	SendAccountExists(ctx context.Context, to string) error
	// SendPasswordChanged tells to that their password changed.
	SendPasswordChanged(ctx context.Context, to string) error
	// SendTwoFactorEnabled tells to that two-factor authentication was
	// turned on.
	SendTwoFactorEnabled(ctx context.Context, to string) error
	// SendTwoFactorDisabled tells to that two-factor authentication was
	// turned off.
	SendTwoFactorDisabled(ctx context.Context, to string) error
	// SendRecoveryCodeUsed tells to that a recovery code was used to sign in
	// and how many are left.
	SendRecoveryCodeUsed(ctx context.Context, to string, remaining int) error
	// SendSignInMethodAdded tells to that a sign-in method such as Google was
	// linked to their account.
	SendSignInMethodAdded(ctx context.Context, to, method string) error
	// SendSignInMethodRemoved tells to that a sign-in method was unlinked.
	SendSignInMethodRemoved(ctx context.Context, to, method string) error
	// SendPasskeyAdded tells to that a passkey named name was added.
	SendPasskeyAdded(ctx context.Context, to, name string) error
	// SendPasskeyRemoved tells to that a passkey named name was removed.
	SendPasskeyRemoved(ctx context.Context, to, name string) error
}
```

Emails sends the emails authentication needs. Apps use [NewMailEmails](#NewMailEmails) or their own templates. Implementations should queue rather than deliver inline (jobs.AsyncSender), so requests don't wait for the provider.

#### func NewMailEmails

```go
func NewMailEmails(sender mail.Sender, appName string) Emails
```

NewMailEmails returns plain, readable emails sent through sender, which sets the From address (mail.WithDefaults). appName appears in subjects and bodies.

### type Hasher

```go
type Hasher struct {
	// contains filtered or unexported fields
}
```

Hasher hashes and verifies passwords with argon2id, bounding how many hashes run at once so a burst of logins can't exhaust memory. It is safe for concurrent use.

#### func NewHasher

```go
func NewHasher() (*Hasher, error)
```

NewHasher returns a hasher with the current parameters.

#### func (*Hasher) Hash

```go
func (h *Hasher) Hash(password string) (string, error)
```

Hash returns an encoded hash: $argon2id$v=19$m=…,t=…,p=…$salt$key.

#### func (*Hasher) Verify

```go
func (h *Hasher) Verify(password, encoded string) (ok, rehash bool)
```

Verify reports whether password matches encoded, and whether the hash should be replaced (Hash again and store it) because its parameters are outdated.

#### func (*Hasher) VerifyDummy

```go
func (h *Hasher) VerifyDummy(password string)
```

VerifyDummy does the work of a failed Verify. Call it when an account doesn't exist, so the response takes as long as for a wrong password.

### type Keyring

```go
type Keyring struct {
	// contains filtered or unexported fields
}
```

A Keyring encrypts small secrets, such as TOTP secrets, with AES-256-GCM. It holds one or more keys, each with an ID: the first key encrypts, and every key decrypts, so a key can be replaced without a flag day (put the new key first, re-encrypt, then remove the old one).

#### func ParseKeyring

```go
func ParseKeyring(spec string) (*Keyring, error)
```

ParseKeyring parses keys written as comma-separated "id:base64key" entries, each key 32 bytes, the first used to encrypt. IDs are lowercase letters, digits, hyphens and underscores (at most 32).

#### func (*Keyring) CurrentKeyID

```go
func (k *Keyring) CurrentKeyID() string
```

CurrentKeyID returns the ID of the key that encrypts.

#### func (*Keyring) Decrypt

```go
func (k *Keyring) Decrypt(keyID string, ciphertext, additionalData []byte) ([]byte, error)
```

Decrypt opens a ciphertext from [Keyring.Encrypt](#Keyring.Encrypt) with the key keyID and the same additionalData. It returns [ErrUnknownKey](#ErrUnknownKey) or [ErrDecrypt](#ErrDecrypt).

#### func (*Keyring) Encrypt

```go
func (k *Keyring) Encrypt(plaintext, additionalData []byte) (keyID string, ciphertext []byte, err error)
```

Encrypt seals plaintext with the current key, bound to additionalData (for example the owning user's ID and the secret's purpose), and returns the key's ID and the nonce followed by the ciphertext.

### type Limits

```go
type Limits struct {
	Min, Max time.Duration
}
```

Limits bounds a duration. Apps read durations from runtime settings and clamp them, so a setting can't weaken security beyond the limits.

#### func (Limits) Clamp

```go
func (l Limits) Clamp(d time.Duration) time.Duration
```

Clamp returns d within the limits.

### type MiddlewareOption

```go
type MiddlewareOption interface {
	// contains filtered or unexported methods
}
```

A MiddlewareOption configures [Middleware](#Middleware).

#### func WithCookieName

```go
func WithCookieName(name string) MiddlewareOption
```

WithCookieName sets the session cookie name. Default: [DefaultCookieName](#DefaultCookieName).

#### func WithLogger

```go
func WithLogger(logger *slog.Logger) MiddlewareOption
```

WithLogger sets the logger for authentication failures. Default: discard.

### type PasswordChecker

```go
type PasswordChecker func(ctx context.Context, password string) error
```

A PasswordChecker rejects a new password by returning an error whose message explains why, such as "appears in a list of breached passwords". It must return nil when it can't decide, for example when a remote list is unreachable.

### type PasswordError

```go
type PasswordError struct {
	Reason string
}
```

PasswordError explains why a password was rejected. Reason never contains the password.

#### func (*PasswordError) Error

```go
func (e *PasswordError) Error() string
```


#### func (*PasswordError) Unwrap

```go
func (e *PasswordError) Unwrap() error
```

Unwrap returns [ErrWeakPassword](#ErrWeakPassword).

### type Permission

```go
type Permission struct {
	Name        string
	Description string
}
```

Permission is a declared permission.

### type Principal

```go
type Principal struct {
	UserID    string
	SessionID string
	// Permissions are granted by the user's roles to this session.
	Permissions []string
	// StepUp are permissions of roles requiring two-factor authentication,
	// held back because the session isn't verified with a second factor.
	StepUp []string
	// MFAVerified reports whether the session was verified with a second
	// factor, and MFAVerifiedAt when it last was.
	MFAVerified   bool
	MFAVerifiedAt time.Time
	// SignedInAt is when the session started.
	SignedInAt time.Time
}
```

Principal is the authenticated user of a request.

#### func PrincipalFrom

```go
func PrincipalFrom(ctx context.Context) (Principal, bool)
```

PrincipalFrom returns the authenticated principal in ctx.

#### func (Principal) RecentlySignedIn

```go
func (p Principal) RecentlySignedIn(now time.Time) bool
```

RecentlySignedIn reports whether the session started less than [RecentVerification](#RecentVerification) before now.

#### func (Principal) RecentlyVerified

```go
func (p Principal) RecentlyVerified(now time.Time) bool
```

RecentlyVerified reports whether the session verified a second factor less than [RecentVerification](#RecentVerification) before now. A stolen session that verified one long ago doesn't pass.

### type Role

```go
type Role struct {
	Name        string
	Description string
	Permissions []string
}
```

Role is a named set of permissions.

