auth

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 29, 2026 License: MPL-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package auth carries the request-scoped auth context plus its helpers. Subpackages (signature/, registry/, policy/, nonces) build on this.

Index

Constants

View Source
const SessionCookieName = "txco_session"

sessionCookieName is the literal `Set-Cookie: <name>=...` token the chassis emits and reads. Exported as a package constant so the admin server (which sets the cookie) and the middleware (which reads it) agree.

Variables

View Source
var ErrReplay = errors.New("nonce_replay")

ErrReplay signals the nonce was already used for this key.

Functions

func EightWordSecret

func EightWordSecret() (string, error)

EightWordSecret returns a hyphen-joined 8-word secret drawn from effLongWords (the EFF "long" wordlist, hyphenless variant, 7772 entries). ~103 bits of entropy.

Use this for any credential that authenticates an unsigned-consume endpoint: the first-boot admin bootstrap secret, admin-issued invitation tokens, and any future "anyone-with-the-string-can-mint- creds" pattern. 32-bit shortcuts (4 words from a small list) are not enough — the consume endpoints are the most exposed HTTP surface, and TTL + burn-after-use are belt-and-suspenders, not a substitute for entropy.

func Middleware

func Middleware(cfg Config, next http.Handler) http.Handler

Middleware returns an http.Handler middleware that authenticates requests per Config.Mode and attaches an auth.Context to the request before calling next.

Per the design doc, /healthz and /auth/dev/enroll bypass the auth middleware entirely — they're wired in front of this in server.go. Everything else goes through here.

func WithContext

func WithContext(parent context.Context, c *Context) context.Context

WithContext returns a copy of parent carrying the given auth.Context.

func WriteForbidden

func WriteForbidden(w http.ResponseWriter, code string)

WriteForbidden is a small helper for handler-side capability checks so they can produce the same error shape as the middleware.

Types

type AuthMode

type AuthMode string

AuthMode controls which auth flavours the middleware accepts.

Basic:   only HTTP Basic; signed requests are rejected with 401
Signed:  only RFC 9421 signed; Basic is rejected with 401
Both:    accept whichever the request presents (preferring signed)

In Basic mode with empty admin user/pass, the chassis falls through to "open dev" (current pre-auth behavior).

Browser session cookies (`txco_session=<id>`) are accepted on top of any mode whenever Config.Sessions is non-nil — they're additive, not a mode of their own. The cookie path resolves the session through Sessions.GetSession and stamps a browser-Source auth.Context. CSRF is enforced via SameSite=Strict at the cookie level plus an Origin allowlist check on mutation methods (handled here).

const (
	ModeBasic  AuthMode = "basic"
	ModeSigned AuthMode = "signed"
	ModeBoth   AuthMode = "both"
)

type Config

type Config struct {
	Mode AuthMode

	// Basic-auth credentials. When both are empty AND Mode != Signed,
	// the middleware enters open-dev mode for backward compat.
	BasicUser string
	BasicPass string

	// Signed-request inputs.
	Registry *registry.Registry
	Verifier signature.Verifier
	Nonces   *NonceStore
	Skew     time.Duration // clock-skew window for `created`; defaults to 5min

	// Browser-session inputs. When Sessions is non-nil, the middleware
	// also accepts `Cookie: txco_session=<id>`. AllowedOrigins lists
	// the Origin header values acceptable on mutation requests (POST/
	// PUT/PATCH/DELETE) carrying a session cookie — used as a CSRF
	// belt-and-braces on top of SameSite=Strict. An empty allowlist
	// disables the Origin check (only safe in tests).
	Sessions       SessionStore
	AllowedOrigins []string

	// Logger is optional; when nil the middleware stays quiet.
	Logger func(msg string, fields map[string]any)
}

Config bundles everything the middleware needs to authenticate a request. Built once at admin server startup and reused per request.

type Context

type Context struct {
	Source  string
	ActorID string
	KeyID   string
	// Tenant is the verified actor's legacy `actors.tenant` column.
	// As of phase 1 of the multi-tenancy work, scoping is mediated by
	// the URL's tenant prefix (TenantSlug/TenantID below) and by
	// memberships rather than this column. Kept readable for tooling
	// that hasn't migrated yet; do not introduce new readers.
	Tenant string
	// TenantSlug and TenantID are set by the admin server's tenant
	// resolver middleware when the request path is `/v1/tenants/{t}/…`.
	// Empty on chassis-wide endpoints (/auth/whoami, /healthz, etc.).
	// Phase 3 makes RequireCapability gate on these.
	TenantSlug string
	TenantID   string
	// SuperAdmin mirrors `actors.super_admin`. When true,
	// RequireCapability short-circuits to allow regardless of
	// memberships.
	SuperAdmin   bool
	Capabilities []string
}

Context is what the auth middleware attaches to each authenticated request. Handlers read it via FromContext and pass it to policy.RequireCapability.

`Source` distinguishes how the caller authenticated:

  • "signed": RFC 9421 signed request from an enrolled actor
  • "basic": legacy HTTP basic auth (synthetic admin:all)
  • "open": no auth required (dev mode with --admin-user="")

Basic-auth and open callers carry a non-empty Source but an empty ActorID — there's no actor record on disk for them; the capabilities are synthesized in-memory. Signed callers always have ActorID set.

func FromContext

func FromContext(ctx context.Context) *Context

FromContext returns the auth.Context attached by middleware, or nil if none is set (which means the request bypassed auth, e.g. /healthz).

func (*Context) IsSigned

func (c *Context) IsSigned() bool

IsSigned reports whether the caller authenticated with a signed request (as opposed to basic auth or open dev mode).

type NonceStore

type NonceStore struct {
	// contains filtered or unexported fields
}

NonceStore backs replay-defense. The middleware calls Use on every signed request; ErrReplay means the (key, nonce) pair was already seen within its TTL.

Backed by an in-process sliding-window structure: N+1 buckets that rotate every ttl/N. Inserts go into the head bucket; lookups scan all buckets; on rotation, the oldest bucket is cleared wholesale, which gives O(1) eviction without a GC walk.

Per the design note in db/schema/sqlite/0005_auth.sql, nonces are local-only replay protection — never replicated, never persisted. Restart loss is bounded by the signature's created/expires freshness window (≤ Skew, typically 60s), so memory-only is the natural fit.

func NewNonceStore

func NewNonceStore(ttl time.Duration) *NonceStore

NewNonceStore returns a NonceStore that retains (key_id, nonce) pairs for at least ttl. ttl ≤ 0 falls back to 10 minutes.

func (*NonceStore) CheckFunc

func (s *NonceStore) CheckFunc(ctx context.Context) func(keyID, nonce string) error

CheckFunc returns a closure that wraps Use, matching the signature expected by signature.VerifyOptions.NonceCheck.

func (*NonceStore) Use

func (s *NonceStore) Use(_ context.Context, keyID, nonce string) error

Use records the nonce as seen. Returns nil on success, ErrReplay if the same (key, nonce) pair is already on file (within TTL).

The ctx argument is unused — the in-memory path has no I/O — but stays on the signature so the middleware closure at chassis/auth/middleware.go doesn't need to change.

type SessionStore

type SessionStore interface {
	GetSession(ctx context.Context, sessionID string) (*registry.Session, error)
	TouchSession(ctx context.Context, sessionID string, now time.Time) error
}

SessionStore is the minimal slice of registry behaviour the middleware needs to authenticate a browser session cookie. Keeping this as an interface (rather than importing the full registry type) avoids a cycle and lets tests swap in a fake.

Directories

Path Synopsis
Package policy gates handlers behind capability checks.
Package policy gates handlers behind capability checks.
Package registry reads (and minimally mutates) the actor / actor_keys / tenants / actor_memberships tables.
Package registry reads (and minimally mutates) the actor / actor_keys / tenants / actor_memberships tables.
Package signature is the chassis-owned wrapper around RFC 9421 HTTP message signatures.
Package signature is the chassis-owned wrapper around RFC 9421 HTTP message signatures.
Package throttle is a small per-key fixed-window rate limiter.
Package throttle is a small per-key fixed-window rate limiter.

Jump to

Keyboard shortcuts

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