Help us shape the future of Go! Take the 2026 Go Developer Survey

auth

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package auth provides authentication and authorization functionality.

Package auth provides authentication utilities including password hashing, sessions and API keys.

Index

Constants

View Source
const (
	ResetTokenTTL  = time.Hour
	InviteTokenTTL = 7 * 24 * time.Hour
)

Token lifetimes and the per-user cap on reset mails.

View Source
const DefaultRateLimit = 1000

DefaultRateLimit is the hourly request limit applied when none (or a non-positive one) is configured.

View Source
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

View Source
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.

View Source
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")
)
View Source
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.

View Source
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.

View Source
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.

View Source
var ErrInvalidToken = errors.New("invalid token")

ErrInvalidToken is returned when a session or API-key credential is not valid.

View Source
var ErrSetupDone = errors.New("setup already completed")

ErrSetupDone is returned by CreateFirstOwner when a user already exists.

View Source
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

func GenerateRandomString(length int) (string, error)

GenerateRandomString creates a cryptographically secure random string.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword creates an Argon2id hash of the password.

func HashPasswordContext

func HashPasswordContext(ctx context.Context, password string) (string, error)

HashPasswordContext is HashPassword that stops waiting for a hashing slot when ctx is cancelled.

func IsValidScope

func IsValidScope(scope string) bool

IsValidScope checks if a scope string is valid.

func NormalizeRateLimit

func NormalizeRateLimit(n int) int

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

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

VerifyPassword checks if a password matches the hash.

func VerifyPasswordContext

func VerifyPasswordContext(ctx context.Context, password, encodedHash string) (bool, error)

VerifyPasswordContext is VerifyPassword that stops waiting for a hashing slot when ctx is cancelled (it then returns ctx's error and does not 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) CanRead

func (k *APIKey) CanRead(resource string) bool

CanRead checks if the key has read access to a resource type.

func (*APIKey) CanWrite

func (k *APIKey) CanWrite(resource string) bool

CanWrite checks if the key has write access to a resource type.

func (*APIKey) CreatorCanAct

func (k *APIKey) CreatorCanAct() bool

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.

func ValidScopes

func ValidScopes() []APIKeyScope

ValidScopes returns all valid 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) Delete

func (s *APIKeyService) Delete(ctx context.Context, id uuid.UUID) error

Delete permanently removes an API key.

func (*APIKeyService) GetByID

func (s *APIKeyService) GetByID(ctx context.Context, id uuid.UUID) (*APIKey, error)

GetByID retrieves an API key by ID.

func (*APIKeyService) List

func (s *APIKeyService) List(ctx context.Context) ([]*APIKey, error)

List retrieves all API keys.

func (*APIKeyService) RecordUsage

func (s *APIKeyService) RecordUsage(ctx context.Context, keyID uuid.UUID, ipAddress string) error

RecordUsage updates the last used timestamp and increments the request count.

func (*APIKeyService) Regenerate

func (s *APIKeyService) Regenerate(ctx context.Context, id uuid.UUID) (string, error)

Regenerate creates a new API key while keeping the same record. Returns the new plain key (shown once).

func (*APIKeyService) Revoke

func (s *APIKeyService) Revoke(ctx context.Context, id uuid.UUID, revokedBy *uuid.UUID) error

Revoke marks an API key as revoked.

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

func (s *APIKeyService) ValidateKey(ctx context.Context, plainKey string) (*APIKey, error)

ValidateKey validates an API key and returns the associated key record. This is the main authentication method for API key auth.

type APIKeyWithSecret

type APIKeyWithSecret struct {
	APIKey
	PlainKey string `json:"key"` // Only returned on creation
}

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 NewBootstrap

func NewBootstrap(db *database.DB) *Bootstrap

NewBootstrap builds the service.

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

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

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

func (s *Service) ListTenantMemberships(ctx context.Context, email string) ([]TenantInfo, error)

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

func (s *Service) NeverLock(email string)

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

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

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

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

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.

func (*Service) ValidateSession

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

ValidateSession looks up a server-side admin session by its opaque token. It accepts the admin kind only; see ValidateBearerSession and ValidateAPICookieSession for the JSON API.

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

func (t *UserTokens) IssueReset(ctx context.Context, userID uuid.UUID) (string, error)

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

func (t *UserTokens) PeekInvite(ctx context.Context, token string) (*Invite, error)

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

func (t *UserTokens) RevokeInvite(ctx context.Context, id uuid.UUID) error

RevokeInvite deletes a pending invite.

Jump to

Keyboard shortcuts

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