Documentation
¶
Overview ¶
Package authlocal implements username+password authentication for kombify backends backed by a single break-glass admin record.
Self-hosted kombify tools (TechStack, Sim, …) deliberately avoid running their own multi-user management. End users live in an OIDC provider (Pocket ID, Auth0, PocketBase) once one is provisioned. The local package only exists to bootstrap the very first admin so the operator can log in before any IdP is configured.
Pattern:
- bcrypt.DefaultCost password hashing
- HS256 session JWT minted by authsession.Manager (same cookie as OIDC)
- Show-once break-glass password persisted with a 1h envelope
- First-claim-wins handover from bootstrap to operator-owned creds
Donor: kombify-Techstack/pkg/v2/auth/local (lifted 2026-05-03 and rebound from techstack-internal session/authmw to shared authsession + oidcclient).
Index ¶
- Constants
- Variables
- type ClaimRequest
- type Config
- type Handlers
- type HandlersOption
- type MethodsResponse
- type ProviderInfo
- type Record
- type RevealResult
- type Service
- func (s *Service) Authenticate(ctx context.Context, email, password string) (authsession.Claims, error)
- func (s *Service) Bootstrap(ctx context.Context) (string, error)
- func (s *Service) Claim(ctx context.Context, req ClaimRequest) error
- func (s *Service) CookieName() string
- func (s *Service) CookieSecure() bool
- func (s *Service) CurrentStatus(ctx context.Context) (Status, error)
- func (s *Service) Reveal(ctx context.Context) (*RevealResult, error)
- type Status
- type Store
Constants ¶
const BreakGlassEmail = "breakglass@local"
BreakGlassEmail is the well-known email of the auto-bootstrapped admin.
const BreakGlassRecordID = "breakglassroot0"
BreakGlassRecordID is the singleton row id used by PocketBase-backed stores (must be at least 15 characters to satisfy PB id min-length).
const DefaultProviderID = "breakglass"
DefaultProviderID is the logical provider id the local credential method returns through the discovery endpoint.
const PasswordEnvelopeTTL = 1 * time.Hour
PasswordEnvelopeTTL is how long the auto-generated break-glass password is retrievable via Reveal() before being permanently scrubbed.
Variables ¶
var ( ErrInvalidConfig = errors.New("authlocal: invalid configuration") ErrInvalidCreds = errors.New("authlocal: invalid credentials") ErrAlreadyClaimed = errors.New("authlocal: break-glass admin already claimed") ErrNotFound = errors.New("authlocal: break-glass admin not initialized") ErrPasswordExpired = errors.New("authlocal: break-glass password no longer available") ErrLockedByOperator = errors.New("authlocal: break-glass claim is locked") )
Errors.
Functions ¶
This section is empty.
Types ¶
type ClaimRequest ¶
type ClaimRequest struct {
// CurrentPassword is the bootstrap password the operator received.
CurrentPassword string
// NewEmail optionally renames the admin to a personal address.
NewEmail string
// NewPassword is the operator's chosen permanent secret.
NewPassword string
}
ClaimRequest is the input to Service.Claim.
type Config ¶
type Config struct {
// Store backs the singleton break-glass record. Required.
Store Store
// Sessions mints HS256 session JWTs identical to the OIDC callback path.
// Required.
Sessions *authsession.Manager
// DefaultTenantID is stamped into the session claims for break-glass
// logins. Defaults to "default".
DefaultTenantID string
// BootstrapEmail overrides the default break-glass email
// ([BreakGlassEmail]) used by [Service.Bootstrap].
BootstrapEmail string
// SessionCookieName must match the cookie used by the OIDC flow so a
// break-glass session is indistinguishable from an OIDC session.
// Defaults to [authsession.DefaultSessionCookieName].
SessionCookieName string
// SessionCookieSecure controls the Secure flag on auth cookies.
SessionCookieSecure bool
// ClaimLocked refuses claims even when the record is still in bootstrap
// state. Used to harden production after the legitimate admin has
// claimed.
ClaimLocked bool
// Now is injectable for tests.
Now func() time.Time
}
Config configures a Service.
type Handlers ¶
type Handlers struct {
// contains filtered or unexported fields
}
Handlers groups HTTP handlers for the local credential service plus the methods-discovery endpoint that lists OIDC providers + break-glass.
func NewHandlers ¶
func NewHandlers(svc *Service, registry *oidcclient.Registry, opts ...HandlersOption) *Handlers
NewHandlers constructs HTTP handlers around the Service. Pass an oidcclient.Registry (or nil) to advertise OIDC providers alongside the break-glass entry in the methods response.
func (*Handlers) ClaimHandler ¶
ClaimHandler returns a handler for POST /api/v1/auth/breakglass/claim.
func (*Handlers) LoginHandler ¶
LoginHandler returns a handler for POST /api/v1/auth/login.
func (*Handlers) LogoutHandler ¶
LogoutHandler returns a handler for POST /api/v1/auth/logout.
func (*Handlers) MethodsHandler ¶
MethodsHandler returns a handler for GET /api/v1/auth/methods.
func (*Handlers) RevealHandler ¶
RevealHandler returns a handler for GET /api/v1/auth/breakglass/reveal.
type HandlersOption ¶
type HandlersOption func(*Handlers)
HandlersOption configures NewHandlers.
func WithLoginRedirectPath ¶
func WithLoginRedirectPath(prefix string) HandlersOption
WithLoginRedirectPath overrides the default `auth_url` prefix returned to the frontend for OIDC providers.
type MethodsResponse ¶
type MethodsResponse struct {
Providers []ProviderInfo `json:"providers"`
BreakGlass Status `json:"breakglass"`
}
MethodsResponse is returned by GET /api/v1/auth/methods (or similar).
type ProviderInfo ¶
type ProviderInfo struct {
ID string `json:"id"`
Kind string `json:"kind"`
Label string `json:"label"`
// AuthURL is the relative URL the frontend should navigate to in order
// to start an OIDC redirect. Empty for local providers.
AuthURL string `json:"auth_url,omitempty"`
}
ProviderInfo is one entry in the unified methods response.
type Record ¶
type Record struct {
// Email is the login identifier. Bootstrap creates it as
// [BreakGlassEmail] (or [Config.BootstrapEmail]) but Claim() lets the
// operator rename it.
Email string
// PasswordHash is bcrypt(password). Empty means the record has not been
// initialised yet.
PasswordHash string
// ShowPassword carries the plaintext password while the envelope is
// alive. Empty after Reveal() consumed it or PasswordEnvelopeTTL elapsed.
ShowPassword string
// ShowUntil is the envelope expiry. Zero means no pending envelope.
ShowUntil time.Time
// Claimed reports whether the operator has rotated the bootstrap secret
// at least once via Claim(). After claim, Reveal() returns 410.
Claimed bool
// CreatedAt and UpdatedAt are auto-stamped by the store.
CreatedAt time.Time
UpdatedAt time.Time
}
Record is the break-glass admin singleton record.
type RevealResult ¶
type RevealResult struct {
Email string `json:"email"`
Password string `json:"password"`
ExpiresAt time.Time `json:"expires_at"`
Claimed bool `json:"claimed"`
}
RevealResult is the show-once envelope payload.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the local credential service.
func (*Service) Authenticate ¶
func (s *Service) Authenticate(ctx context.Context, email, password string) (authsession.Claims, error)
Authenticate verifies a login attempt and returns authsession.Claims if valid.
func (*Service) Bootstrap ¶
Bootstrap ensures the break-glass admin record exists.
On first run it creates a record with the configured BootstrapEmail and a freshly generated 18-byte password (~24 char base64), persists the bcrypt hash, and stores the plaintext in a 1h show-once envelope. On subsequent runs it is a no-op.
Returns the generated plaintext password if a new record was created, otherwise the empty string.
func (*Service) Claim ¶
func (s *Service) Claim(ctx context.Context, req ClaimRequest) error
Claim rotates the break-glass record from bootstrap state to its final operator-owned values. First-POST-wins: subsequent calls return ErrAlreadyClaimed.
If Config.ClaimLocked is set, claiming is refused even when the record is still in bootstrap state — used to harden production where the operator has already claimed and wants to make absolutely sure no second claim slips in.
func (*Service) CookieName ¶
CookieName returns the configured session cookie name.
func (*Service) CookieSecure ¶
CookieSecure returns whether the session cookie is marked Secure.
func (*Service) CurrentStatus ¶
CurrentStatus inspects the record without exposing secrets.
func (*Service) Reveal ¶
func (s *Service) Reveal(ctx context.Context) (*RevealResult, error)
Reveal returns the bootstrap password if the envelope is still alive. Calling Reveal on an expired or already-consumed envelope returns ErrPasswordExpired.
type Status ¶
type Status struct {
Initialized bool `json:"initialized"`
Claimed bool `json:"claimed"`
Email string `json:"email,omitempty"`
HasPendingReveal bool `json:"has_pending_reveal"`
RevealExpiresAt time.Time `json:"reveal_expires_at,omitempty"`
Locked bool `json:"locked"`
}
Status reports the current bootstrap/claim state used by the methods discovery endpoint and the setup wizard.
type Store ¶
type Store interface {
// Get returns the current record or nil if it has never been written.
Get(ctx context.Context) (*Record, error)
// Save creates or replaces the singleton record.
Save(ctx context.Context, r *Record) error
}
Store persists the singleton break-glass admin record.
Implementations must serialise concurrent writes; Get may return a copy. Save replaces the entire record (single-row collection semantics).