Documentation
¶
Overview ¶
Package auth provides authentication and authorization functionality.
Package auth provides authentication utilities including password hashing, sessions and API keys.
Index ¶
- Constants
- Variables
- func ClearAPISessionCookie(w http.ResponseWriter, secure bool)
- func ClearSessionCookie(w http.ResponseWriter, secure bool)
- func GenerateRandomString(length int) (string, error)
- func HashPassword(password string) (string, error)
- func HashPasswordContext(ctx context.Context, password string) (string, error)
- func IsValidScope(scope string) bool
- func NormalizeRateLimit(n int) int
- func SetAPISessionCookie(w http.ResponseWriter, token string, secure bool)
- func SetSessionCookie(w http.ResponseWriter, token string, secure bool)
- func UseNeutralCookieNames(secure bool)
- func VerifyPassword(password, encodedHash string) (bool, error)
- func VerifyPasswordContext(ctx context.Context, password, encodedHash string) (bool, error)
- type APIKey
- type APIKeyScope
- type APIKeyService
- func (s *APIKeyService) Create(ctx context.Context, createdBy *uuid.UUID, input *CreateAPIKeyInput) (*APIKeyWithSecret, error)
- func (s *APIKeyService) Delete(ctx context.Context, id uuid.UUID) error
- func (s *APIKeyService) GetByID(ctx context.Context, id uuid.UUID) (*APIKey, error)
- func (s *APIKeyService) List(ctx context.Context) ([]*APIKey, error)
- func (s *APIKeyService) RecordUsage(ctx context.Context, keyID uuid.UUID, ipAddress string) error
- func (s *APIKeyService) Regenerate(ctx context.Context, id uuid.UUID) (string, error)
- func (s *APIKeyService) Revoke(ctx context.Context, id uuid.UUID, revokedBy *uuid.UUID) error
- func (s *APIKeyService) Update(ctx context.Context, id uuid.UUID, input *UpdateAPIKeyInput) (*APIKey, error)
- func (s *APIKeyService) ValidateKey(ctx context.Context, plainKey string) (*APIKey, error)
- type APIKeyWithSecret
- type Bootstrap
- type Claims
- type CreateAPIKeyInput
- type Invite
- type LoginRequest
- type LoginResponse
- type Service
- func (s *Service) ConfirmPassword(ctx context.Context, userID uuid.UUID, email, password string) error
- func (s *Service) IssueSession(ctx context.Context, userID uuid.UUID, userAgent, ip string) (string, error)
- func (s *Service) ListTenantMemberships(ctx context.Context, email string) ([]TenantInfo, error)
- func (s *Service) Login(ctx context.Context, req *LoginRequest, userAgent, ip string) (*LoginResponse, error)
- func (s *Service) LoginAPISession(ctx context.Context, email, password, userAgent, ip string) (*user.User, string, error)
- func (s *Service) LoginSession(ctx context.Context, email, password, userAgent, ip string) (*user.User, string, error)
- func (s *Service) NeverLock(email string)
- func (s *Service) ValidateAPICookieSession(ctx context.Context, token string) (*Claims, error)
- func (s *Service) ValidateBearerSession(ctx context.Context, token string) (*Claims, error)
- func (s *Service) ValidateSession(ctx context.Context, token string) (*Claims, error)
- type TenantInfo
- type TenantLookup
- type UpdateAPIKeyInput
- type UserTokens
- func (t *UserTokens) ChangePassword(ctx context.Context, userID uuid.UUID, current, newPassword string, ...) error
- func (t *UserTokens) ConsumeInvite(ctx context.Context, token, name, password string) (*user.User, error)
- func (t *UserTokens) ConsumeReset(ctx context.Context, token, newPassword string) (*user.User, error)
- func (t *UserTokens) IssueInvite(ctx context.Context, email string, role user.Role, invitedBy uuid.UUID) (string, *Invite, error)
- func (t *UserTokens) IssueReset(ctx context.Context, userID uuid.UUID) (string, error)
- func (t *UserTokens) ListInvites(ctx context.Context) ([]Invite, error)
- func (t *UserTokens) PeekInvite(ctx context.Context, token string) (*Invite, error)
- func (t *UserTokens) PurgeExpired(ctx context.Context) (int64, error)
- func (t *UserTokens) ResetValid(ctx context.Context, token string) bool
- func (t *UserTokens) RevokeInvite(ctx context.Context, id uuid.UUID) error
Constants ¶
const ( ResetTokenTTL = time.Hour InviteTokenTTL = 7 * 24 * time.Hour )
Token lifetimes and the per-user cap on reset mails.
const DefaultRateLimit = 1000
DefaultRateLimit is the hourly request limit applied when none (or a non-positive one) is configured.
const MinPasswordLen = 12
MinPasswordLen is the shortest password the admin accepts (also used by the CLI): length is the only rule, no composition requirements.
Variables ¶
var ( ErrAPIKeyNotFound = errors.New("api key not found") ErrAPIKeyExpired = errors.New("api key has expired") ErrAPIKeyRevoked = errors.New("api key has been revoked") ErrAPIKeyRateLimit = errors.New("api key rate limit exceeded") ErrAPIKeyInvalidHash = errors.New("invalid api key") ErrAPIKeyCreatorGone = errors.New("api key creator is missing or no longer an owner/editor") ErrInvalidScope = errors.New("invalid scope") ErrInvalidRateLimit = errors.New("rate_limit must be a positive integer") )
API key errors.
var ( // ErrInvalidHash indicates the hash format is invalid. ErrInvalidHash = errors.New("invalid password hash format") // ErrIncompatibleVersion indicates an unsupported Argon2 version. ErrIncompatibleVersion = errors.New("incompatible argon2 version") )
var ( ErrInvalidCredentials = errors.New("invalid email or password") ErrUserNotFound = errors.New("user not found") ErrUserDisabled = errors.New("user account is disabled") // ErrAccountLocked is returned when an account is temporarily locked due to // too many failed login attempts. ErrAccountLocked = errors.New("account temporarily locked due to failed login attempts") )
Common authentication errors.
var ( ErrTokenInvalid = errors.New("invalid or expired token") ErrInvalidEmail = errors.New("enter a valid email address") ErrPasswordShort = fmt.Errorf("password must be at least %d characters", MinPasswordLen) ErrResetThrottle = errors.New("too many password reset requests; try again later") )
Errors of the one-time token flows. ErrTokenInvalid deliberately covers unknown, expired and already-used tokens alike.
var APISessionCookieName = "hatamoto_api_session"
APISessionCookieName is the HttpOnly cookie that carries the JSON API session token (kind api), used only when cookie auth is enabled. It is a separate credential from SessionCookieName and is renamed together with it by UseNeutralCookieNames.
var ErrInvalidToken = errors.New("invalid token")
ErrInvalidToken is returned when a session or API-key credential is not valid.
var ErrSetupDone = errors.New("setup already completed")
ErrSetupDone is returned by CreateFirstOwner when a user already exists.
var SessionCookieName = "hatamoto_session"
SessionCookieName is the HttpOnly cookie that carries the opaque admin session token. The default is the historical name (tests and library users keep it); the server switches to the neutral name once at boot through UseNeutralCookieNames, before any request is served. Not safe to change afterwards.
Functions ¶
func ClearAPISessionCookie ¶
func ClearAPISessionCookie(w http.ResponseWriter, secure bool)
ClearAPISessionCookie expires the JSON API session cookie.
func ClearSessionCookie ¶
func ClearSessionCookie(w http.ResponseWriter, secure bool)
ClearSessionCookie expires the admin session cookie (logout / invalid session). Legacy-named cookies are expired by the server's legacy cookie middleware, so no response names the old product cookie unprompted.
func GenerateRandomString ¶
GenerateRandomString creates a cryptographically secure random string.
func HashPassword ¶
HashPassword creates an Argon2id hash of the password.
func HashPasswordContext ¶
HashPasswordContext is HashPassword that stops waiting for a hashing slot when ctx is cancelled.
func IsValidScope ¶
IsValidScope checks if a scope string is valid.
func NormalizeRateLimit ¶
NormalizeRateLimit maps non-positive limits to DefaultRateLimit.
func SetAPISessionCookie ¶
func SetAPISessionCookie(w http.ResponseWriter, token string, secure bool)
SetAPISessionCookie writes the JSON API session cookie. It carries a kind-api session, a different credential from the admin form cookie, so page script and the admin UI never share one. Attributes match SetSessionCookie.
func SetSessionCookie ¶
func SetSessionCookie(w http.ResponseWriter, token string, secure bool)
SetSessionCookie writes the admin session cookie. HttpOnly (no JS access), Secure (configurable for local HTTP dev), SameSite=Strict (admin + API are same-origin via the Next rewrite, so Strict is safe and strongest), Path=/.
func UseNeutralCookieNames ¶
func UseNeutralCookieNames(secure bool)
UseNeutralCookieNames sets a name that does not identify the software. Secure mode uses the __Host- prefix (browsers require Secure, Path=/ and no Domain, which SetSessionCookie sets). Browsers drop prefixed cookies that lack Secure, so plain-HTTP dev mode uses an unprefixed name.
func VerifyPassword ¶
VerifyPassword checks if a password matches the hash.
Types ¶
type APIKey ¶
type APIKey struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
KeyPrefix string `json:"key_prefix"`
KeyHash string `json:"-"` // Never expose the hash
Scopes []APIKeyScope `json:"scopes"`
RateLimit int `json:"rate_limit"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
LastUsedIP string `json:"last_used_ip,omitempty"`
RequestCount int64 `json:"request_count"`
IsActive bool `json:"is_active"`
CreatedBy *uuid.UUID `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
RevokedAt *time.Time `json:"revoked_at,omitempty"`
RevokedBy *uuid.UUID `json:"revoked_by,omitempty"`
// CreatorRole is the creator's CURRENT users.role, filled by ValidateKey
// only ("" when the user is missing or the key was not validated). A key
// acts with its creator's authority, never more.
CreatorRole string `json:"-"`
}
APIKey represents an API key record.
func (*APIKey) CreatorCanAct ¶
CreatorCanAct reports whether the key's creator still holds a role that may edit content (owner or editor). An unknown or empty role fails closed.
func (*APIKey) HasScope ¶
func (k *APIKey) HasScope(scope APIKeyScope) bool
HasScope checks if an API key has a specific scope.
type APIKeyScope ¶
type APIKeyScope string
APIKeyScope represents a permission scope for an API key.
const ( ScopeReadEntities APIKeyScope = "read:entities" ScopeWriteEntities APIKeyScope = "write:entities" ScopeReadCitations APIKeyScope = "read:citations" ScopeReadReports APIKeyScope = "read:reports" ScopeWriteReports APIKeyScope = "write:reports" ScopeReadQuerySuites APIKeyScope = "read:query_suites" ScopeWriteQuerySuites APIKeyScope = "write:query_suites" // Site-structure write scopes. They are separate from write:entities so // keys issued for publishing content never gain them. ScopeWriteMedia APIKeyScope = "write:media" ScopeWriteMenus APIKeyScope = "write:menus" ScopeWriteRedirects APIKeyScope = "write:redirects" ScopeWriteTerms APIKeyScope = "write:terms" ScopeAll APIKeyScope = "*" )
Available API key scopes.
type APIKeyService ¶
type APIKeyService struct {
// contains filtered or unexported fields
}
APIKeyService manages API keys.
func NewAPIKeyService ¶
func NewAPIKeyService(db *database.DB, logger *slog.Logger) *APIKeyService
NewAPIKeyService creates a new API key service.
func (*APIKeyService) Create ¶
func (s *APIKeyService) Create(ctx context.Context, createdBy *uuid.UUID, input *CreateAPIKeyInput) (*APIKeyWithSecret, error)
Create generates a new API key.
func (*APIKeyService) List ¶
func (s *APIKeyService) List(ctx context.Context) ([]*APIKey, error)
List retrieves all API keys.
func (*APIKeyService) RecordUsage ¶
RecordUsage updates the last used timestamp and increments the request count.
func (*APIKeyService) Regenerate ¶
Regenerate creates a new API key while keeping the same record. Returns the new plain key (shown once).
func (*APIKeyService) Update ¶
func (s *APIKeyService) Update(ctx context.Context, id uuid.UUID, input *UpdateAPIKeyInput) (*APIKey, error)
Update modifies an existing API key.
func (*APIKeyService) ValidateKey ¶
ValidateKey validates an API key and returns the associated key record. This is the main authentication method for API key auth.
type APIKeyWithSecret ¶
APIKeyWithSecret is returned only when creating a new key. The plain key is shown once and never stored.
type Bootstrap ¶
type Bootstrap struct {
// contains filtered or unexported fields
}
Bootstrap creates the first owner of a fresh install.
func (*Bootstrap) CreateFirstOwner ¶
func (b *Bootstrap) CreateFirstOwner(ctx context.Context, name, email, password string) (*user.User, error)
CreateFirstOwner inserts an owner only while the users table is empty. The check and the insert run in one transaction under an advisory lock, so two people racing through the setup wizard cannot both become owner: the loser gets ErrSetupDone.
type Claims ¶
type Claims struct {
UserID uuid.UUID `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
IsSuperadmin bool `json:"is_superadmin,omitempty"`
}
Claims is the authenticated principal a validated session puts in the request context.
type CreateAPIKeyInput ¶
type CreateAPIKeyInput struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Scopes []APIKeyScope `json:"scopes"`
RateLimit int `json:"rate_limit,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
CreateAPIKeyInput contains parameters for creating a new API key.
type Invite ¶
type Invite struct {
ID uuid.UUID
Email string
Role user.Role
InvitedBy *uuid.UUID
ExpiresAt time.Time
CreatedAt time.Time
}
Invite is a pending invitation.
type LoginRequest ¶
LoginRequest represents a login request.
type LoginResponse ¶
type LoginResponse struct {
User *user.User `json:"user"`
SessionToken string `json:"session_token"`
Tenants []TenantInfo `json:"tenants"`
// CSRFToken is set only when cookie auth is on: send it as X-CSRF-Token on
// unsafe requests authenticated by the API session cookie.
CSRFToken string `json:"csrf_token,omitempty"`
}
LoginResponse represents a successful login response. SessionToken is sent as `Authorization: Bearer ...` (or carried by the session cookie).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service handles authentication operations.
func NewService ¶
func NewService(userRepo user.Repository, tenantLookup TenantLookup, sessions *session.Store) *Service
NewService creates a new authentication service. Sessions are the only credential: Login issues one, ValidateSession looks them up.
func (*Service) ConfirmPassword ¶
func (s *Service) ConfirmPassword(ctx context.Context, userID uuid.UUID, email, password string) error
ConfirmPassword re-checks a signed-in user's password for sudo mode. It shares authenticate, so failures count toward the login lockout. The account that email resolves to must be userID, the session's user: any other account is refused as invalid credentials, so a lookup that drifts cannot elevate.
func (*Service) IssueSession ¶
func (s *Service) IssueSession(ctx context.Context, userID uuid.UUID, userAgent, ip string) (string, error)
IssueSession starts a 30-day admin session for userID (after reset, invite acceptance or setup) and returns the cookie value.
func (*Service) ListTenantMemberships ¶
ListTenantMemberships is the exported version for use by handlers.
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, req *LoginRequest, userAgent, ip string) (*LoginResponse, error)
Login authenticates a user with email and password.
userAgent and ip are recorded against the server-side session for auditability ("active sessions" admin view). Pass empty strings if the caller can't determine them; they're optional.
func (*Service) LoginAPISession ¶
func (s *Service) LoginAPISession(ctx context.Context, email, password, userAgent, ip string) (*user.User, string, error)
LoginAPISession authenticates and issues a kind-api session for the JSON API login. It returns the user and the opaque session token, which is both the bearer token and the API cookie value. It never issues an admin session.
func (*Service) LoginSession ¶
func (s *Service) LoginSession(ctx context.Context, email, password, userAgent, ip string) (*user.User, string, error)
LoginSession authenticates and issues a server-side session. It returns the user and the opaque session token to set as the cookie.
func (*Service) NeverLock ¶
NeverLock exempts one account, by email, from the failed-login lockout (the public demo's shared login). The per-address rate limit still applies.
func (*Service) ValidateAPICookieSession ¶
ValidateAPICookieSession validates a token read from the API session cookie. Only the api kind is accepted, so the admin form session never works on the JSON API.
func (*Service) ValidateBearerSession ¶
ValidateBearerSession validates a token presented as a bearer credential: admin and api sessions are both accepted, since a bearer token is never sent by the browser on its own.
type TenantInfo ¶
type TenantInfo struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Role string `json:"role"`
}
TenantInfo represents a tenant membership for the current user.
type TenantLookup ¶
type TenantLookup interface {
GetTenantNameSlug(ctx context.Context) (name, slug string, err error)
}
TenantLookup provides tenant name/slug lookup without importing the tenant package.
type UpdateAPIKeyInput ¶
type UpdateAPIKeyInput struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Scopes []APIKeyScope `json:"scopes,omitempty"`
RateLimit *int `json:"rate_limit,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
UpdateAPIKeyInput contains parameters for updating an API key.
type UserTokens ¶
type UserTokens struct {
// contains filtered or unexported fields
}
UserTokens issues and consumes password-reset and invite tokens. Consuming a token is a single UPDATE ... WHERE used_at IS NULL AND expires_at > now() RETURNING, so a token works exactly once even under concurrent requests.
func NewUserTokens ¶
func NewUserTokens(db *database.DB) *UserTokens
NewUserTokens builds the service.
func (*UserTokens) ChangePassword ¶
func (t *UserTokens) ChangePassword(ctx context.Context, userID uuid.UUID, current, newPassword string, keepSession *uuid.UUID) error
ChangePassword sets a new password after checking the current one, clears the lockout and signs out every other session of the user (keepSession, the caller's own, survives).
func (*UserTokens) ConsumeInvite ¶
func (t *UserTokens) ConsumeInvite(ctx context.Context, token, name, password string) (*user.User, error)
ConsumeInvite creates the invited user (name, password) and burns the token in one transaction.
func (*UserTokens) ConsumeReset ¶
func (t *UserTokens) ConsumeReset(ctx context.Context, token, newPassword string) (*user.User, error)
ConsumeReset sets the new password for the token's user, clears the login lockout, revokes every session of that user and burns the token, all in one transaction. The returned user may be signed in with a fresh session.
func (*UserTokens) IssueInvite ¶
func (t *UserTokens) IssueInvite(ctx context.Context, email string, role user.Role, invitedBy uuid.UUID) (string, *Invite, error)
IssueInvite creates an invite (valid InviteTokenTTL) for email with role. An address that already has an account is rejected with user.ErrEmailAlreadyExists; an earlier pending invite for the same address is replaced.
func (*UserTokens) IssueReset ¶
IssueReset creates a reset token for userID (valid ResetTokenTTL). Earlier unused reset tokens of that user stop working. More than three requests an hour for one user return ErrResetThrottle.
func (*UserTokens) ListInvites ¶
func (t *UserTokens) ListInvites(ctx context.Context) ([]Invite, error)
ListInvites returns the unexpired, unused invites, newest first.
func (*UserTokens) PeekInvite ¶
PeekInvite returns the invite behind a live token without consuming it (the accept page shows the address and role).
func (*UserTokens) PurgeExpired ¶
func (t *UserTokens) PurgeExpired(ctx context.Context) (int64, error)
PurgeExpired removes tokens that expired or were used more than a day ago.
func (*UserTokens) ResetValid ¶
func (t *UserTokens) ResetValid(ctx context.Context, token string) bool
ResetValid reports whether token is a live reset token, without consuming it (the reset page checks it before showing the form).
func (*UserTokens) RevokeInvite ¶
RevokeInvite deletes a pending invite.