authlocal

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

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

View Source
const BreakGlassEmail = "breakglass@local"

BreakGlassEmail is the well-known email of the auto-bootstrapped admin.

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

View Source
const DefaultProviderID = "breakglass"

DefaultProviderID is the logical provider id the local credential method returns through the discovery endpoint.

View Source
const PasswordEnvelopeTTL = 1 * time.Hour

PasswordEnvelopeTTL is how long the auto-generated break-glass password is retrievable via Reveal() before being permanently scrubbed.

Variables

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

func (h *Handlers) ClaimHandler() http.Handler

ClaimHandler returns a handler for POST /api/v1/auth/breakglass/claim.

func (*Handlers) LoginHandler

func (h *Handlers) LoginHandler() http.Handler

LoginHandler returns a handler for POST /api/v1/auth/login.

func (*Handlers) LogoutHandler

func (h *Handlers) LogoutHandler() http.Handler

LogoutHandler returns a handler for POST /api/v1/auth/logout.

func (*Handlers) MethodsHandler

func (h *Handlers) MethodsHandler() http.Handler

MethodsHandler returns a handler for GET /api/v1/auth/methods.

func (*Handlers) RevealHandler

func (h *Handlers) RevealHandler() http.Handler

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
	// initialized 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 New

func New(cfg Config) (*Service, error)

New validates Config and returns a 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

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

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

func (s *Service) CookieName() string

CookieName returns the configured session cookie name.

func (*Service) CookieSecure

func (s *Service) CookieSecure() bool

CookieSecure returns whether the session cookie is marked Secure.

func (*Service) CurrentStatus

func (s *Service) CurrentStatus(ctx context.Context) (Status, error)

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 serialize concurrent writes; Get may return a copy. Save replaces the entire record (single-row collection semantics).

Jump to

Keyboard shortcuts

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