Documentation
¶
Overview ¶
Package webauth embeds the Authula (Apache-2.0, v1.11.0) Go auth framework as the cockpit's "industrial" web-auth provider, behind the AURA_WEB_AUTH_PROVIDER feature flag (see docs/cockpit-overhaul/05-authula-auth-SPEC.md, Option A2). It constructs authula.New with the three mandatory hardenings:
- H1 schema isolation: Authula has NO table-prefix config, so it gets its own dedicated `authula` Postgres schema via a DSN `search_path=authula` and its OWN database/sql pool (lib/pq honours search_path). Aura's aura.* schema and pgxpool are never touched. The `authula` schema is CREATEd by Aura migration 0019; Authula's own bun migrator fills its CONTENTS.
- H2 cookie: __Host-authula_session + Secure + SameSite=Strict + 12h absolute lifetime (Authula defaults Secure=false / SameSite=lax — flipped here).
- H3 CSRF: the csrf plugin (double-submit cookie + Fetch-Metadata header protection) plus a per-cookie-name __Host- CSRF token.
The validate-only seam (RequireAuth's cookie core) lives in session_validate.go; the operator user-id <-> `local` identity binding lives in identity_link.go. This file owns ONLY construction + lifecycle (Close).
identity_link.go owns the operator-user <-> Aura-identity binding (spec §5). Authula authenticates "who you are" (a users.id in the authula schema); Aura authorizes "what agent.run requires" via capability_grants keyed on aura.identities.id. The bridge is a single join row in aura.identity_auth_links(identity_id, authula_user_id UNIQUE), created by migration 0019. For the single-operator cockpit the Authula user-id is pinned to the seeded `local` identity UUID (00000000-…-0001).
The link is queried over Aura's existing pgxpool (NOT bun, NOT the authula pool) — the table lives in aura.*, so it is owned by Aura's migration ledger. The UNIQUE is on authula_user_id (not identity_id) so the schema generalizes 1:N for the post-v1.0.0 multi-user milestone (spec OQ-8).
session_validate.go is the Option-A2 seam (spec §3.2/§4.4/OQ-3): it replaces ONLY the cookie-validation core of agui.RequireAuth when AURA_WEB_AUTH_PROVIDER=authula. The validate path is read verbatim from Authula's own session hook (plugins/session/plugin.go:125-144):
hashed := TokenService.Hash(rawCookieValue) // SHA-256 hex
sess, err := SessionService.GetByToken(ctx, hashed)
if err==nil && sess!=nil && sess.ExpiresAt.After(now.UTC()) { ok }
On a hit it maps the Authula user-id -> the bound Aura identity UUID (identity_link.go) so the existing principalKey{} contract + RequireCapability + capability_grants gate keep working byte-for-byte. There is NO "validate raw cookie" convenience on the SessionService interface, so the seam hashes the cookie itself (services/core.go:30-57).
Index ¶
Constants ¶
const ( CSRFCookieName = "__Host-authula_csrf_token" CSRFHeaderName = "X-AUTHULA-CSRF-TOKEN" )
CSRFCookieName / CSRFHeaderName are the double-submit pair the SPA must echo on every state-changing request (spec §4.7). The header name is Authula's canonical default; the cookie is __Host--prefixed to bind it to the exact origin.
const SessionCookieName = "__Host-authula_session"
SessionCookieName is the hardened __Host--prefixed session cookie Authula sets. The __Host- prefix is honoured because the session plugin always sets Path:/ and no Domain (verified in plugins/session/plugin.go:165-177); Secure must be true, which Config below flips on.
Variables ¶
var ErrLinkNotFound = errors.New("webauth: no aura identity linked to authula user")
ErrLinkNotFound is returned when no identity is bound to the given Authula user-id.
var ErrNoSession = errors.New("webauth: no valid authula session")
ErrNoSession is returned by Validate when the request carries no usable Authula session (missing cookie, unknown token, or expired). The caller (the agui seam) maps it to a 302 login / 401 exactly as the passphrase path does on a bad cookie.
var ErrOperatorAmbiguous = errors.New("webauth: multiple operators enrolled — first-operator auto-pin skipped (multi-user resolves via identity_auth_links)")
ErrOperatorAmbiguous is returned by OperatorUserID when more than one Authula user exists, so the FIRST-operator auto-pin can no longer single out "the" operator. It is a SKIP signal, not a fatal: Phase 28 (prd.md amendment #64, D-07) intentionally enrolls a 2nd web-loginable identity, and the live session-validate path resolves identity 1:N via ResolveIdentityID over aura.identity_auth_links — never via OperatorUserID. The caller treats this sentinel as "leave the existing local link as-is" (no de-pin).
Functions ¶
This section is empty.
Types ¶
type Config ¶
Config is the wiring input for New. DSN is the Postgres connection string for the authula schema; if it lacks a search_path it is forced to search_path=authula so Authula's bare users/sessions/accounts tables can never collide with aura.* or public (H1). Secret is the 32-byte hex Authula uses to derive its HMAC/token keys (AURA_AUTHULA_SECRET). TrustedOrigins seeds the CSRF Fetch-Metadata origin check.
type IdentityLinker ¶
type IdentityLinker struct {
// contains filtered or unexported fields
}
IdentityLinker resolves + upserts the operator-user <-> identity binding over the aura.identity_auth_links table. It satisfies the session_validate.go userResolver seam.
func NewIdentityLinker ¶
func NewIdentityLinker(pool *pgxpool.Pool) *IdentityLinker
NewIdentityLinker binds the linker to Aura's app pool (the same pool the identity Store uses). A nil pool yields ErrLinkNotFound on every resolve (fail closed).
func (*IdentityLinker) LinkOperator ¶
func (l *IdentityLinker) LinkOperator(ctx context.Context, identityID, authulaUserID string) error
LinkOperator idempotently binds an Authula user-id to an Aura identity UUID. It is the enrollment-time call (spec M1) that pins the operator user to the `local` identity. Re-linking the same pair is a no-op; re-pointing an existing authula_user_id updates the bound identity (ON CONFLICT on the UNIQUE authula_user_id).
func (*IdentityLinker) ResolveIdentityID ¶
func (l *IdentityLinker) ResolveIdentityID(ctx context.Context, authulaUserID string) (string, error)
ResolveIdentityID returns the Aura identity UUID bound to the given Authula user-id, or ErrLinkNotFound when no link exists. It is the userResolver implementation the Validator calls on every authenticated request.
type Provider ¶
type Provider struct {
// contains filtered or unexported fields
}
Provider owns the constructed *authula.Auth: its mounted /auth/* HTTP handler, the session-validate seam, and lifecycle Close. It is nil-safe so the passphrase path can leave it unset.
func New ¶
New constructs the embedded Authula provider with the hardened config. It does NOT pass a caller-supplied DB — Authula builds its OWN database/sql pool over the search_path=authula DSN (separate pool, H1 §6.2), running its core + plugin migrations into the authula schema at construction. authula.New panics on any init failure (documented in auth.go), so New recovers and returns a plain error to keep the daemon boot fail-soft rather than crashing the whole process.
func (*Provider) Close ¶
Close releases Authula's plugins + core systems (the expiry cleanup workers) so the daemon shuts down goleak-clean. It is nil-safe (the passphrase path leaves it unset).
func (*Provider) CoreServices ¶
func (p *Provider) CoreServices() *authulaservices.CoreServices
CoreServices exposes Authula's SessionService + TokenService for the validate seam.
func (*Provider) Handler ¶
Handler returns Authula's mounted HTTP handler (all /auth/* credential routes).
func (*Provider) OperatorUserID ¶
OperatorUserID is an ENROLLMENT-TIME helper that auto-pins the FIRST operator to the `local` identity (spec §5; relaxed by prd.md amendment #64 / D-07). It returns "" (no error) when no operator is yet enrolled, the lone user's id when exactly one exists, and ErrOperatorAmbiguous when more than one user is present.
The >1-user case is non-fatal by design: it is NOT a request-path call (the live validate path uses ResolveIdentityID over aura.identity_auth_links, UNIQUE on authula_user_id, 1:N-ready), so a 2nd enrolled identity logs in and resolves with no resolution-path change. The caller skips the auto-pin on ErrOperatorAmbiguous and leaves the already-linked `local` identity untouched (no de-pin, no regression).
type Validator ¶
type Validator struct {
// contains filtered or unexported fields
}
Validator is the cookie-validation core the agui boundary injects in place of the HMAC verifySession when the provider is authula. It owns no HTTP semantics — it returns the Aura identity UUID or ErrNoSession; the agui layer keeps the redirect / 401 / existence-recheck behaviour.
func NewValidator ¶
func NewValidator(p coreServicesProvider, r userResolver) *Validator
NewValidator binds the validate core to the constructed provider + the identity link resolver. Both must be non-nil at the authula composition root.
func (*Validator) Validate ¶
Validate reads the hardened session cookie, hashes it, looks the session up via Authula's SessionService, checks the absolute expiry itself (Authula does the expiry comparison in its hook, not inside GetByToken — OQ-3), then maps the Authula user-id to the Aura identity UUID. Any miss/expiry/decode failure returns ErrNoSession (never a panic) so the seam fails closed.