httpauth

package
v0.8.5 Latest Latest
Warning

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

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

Documentation

Overview

Package httpauth provides authentication policy for the hypercache-server client REST API. It is the v2 successor to the single-token bearerAuth helper that previously lived inside cmd/hypercache-server: that helper supports exactly one shared token with no per-identity granularity and no extension hooks. Real production deployments need multiple tokens (one per consuming service or operator), per-identity scopes (read-only vs read-write vs admin), mTLS as a peer mechanism to bearer auth, and a custom verify hook for JWT/OAuth/etc.

The package is independent of the dist transport's DistHTTPAuth (pkg/backend/dist_http_server.go). Dist auth is intentionally symmetric — every node carries the same token because the cluster is one trust domain — so multi-identity has no operator meaning there. Client API auth is asymmetric (many callers, one server) and benefits from the multi-identity shape this package provides.

Wire-shape: a Policy is loaded once at process start (typically from HYPERCACHE_AUTH_CONFIG / HYPERCACHE_AUTH_TOKEN; see loader.go) and used to build per-route middleware via Policy.Middleware(scope). The middleware's verification path runs in time independent of how many tokens are configured and which one matched (if any) — see the comment on Middleware for the timing-leak considerations.

Index

Constants

View Source
const (
	// EnvAuthConfig points at a YAML file describing tokens
	// and cert identities. Takes precedence over EnvAuthToken.
	EnvAuthConfig = "HYPERCACHE_AUTH_CONFIG"
	// EnvAuthToken is the legacy single-token shortcut: when
	// set, the loader synthesizes one all-scopes TokenIdentity
	// with ID "default" so existing zero-config deployments
	// keep working byte-identical.
	EnvAuthToken = "HYPERCACHE_AUTH_TOKEN" // #nosec G101 -- env-var name, not a credential value
)

Env-var names that drive policy loading. Kept as exported constants so the hypercache-server binary's documentation, the loader, and the tests all reference one canonical name.

View Source
const IdentityKey = "httpauth.identity"

IdentityKey is the fiber.Ctx.Locals key under which the resolved Identity is stored after a successful auth. Handlers that need to attribute audit / metrics can read it back via `c.Locals(httpauth.IdentityKey).(httpauth.Identity)`.

Exported as a typed key so users don't have to remember the stringly-typed name; the type also prevents accidental collisions with unrelated fiber locals.

Variables

View Source
var ErrInvalidPolicy = ewrap.New("httpauth: invalid policy")

ErrInvalidPolicy wraps a policy validation failure. Loaders return this so callers can distinguish "config is wrong" from "filesystem is wrong" (which surfaces as os.PathError) or "secrets backend failed" (caller's concern).

Functions

This section is empty.

Types

type BasicIdentity added in v0.8.3

type BasicIdentity struct {
	Username       string
	PasswordBcrypt []byte // bcrypt-hashed; raw passwords never live here
	ID             string
	Scopes         []Scope
}

BasicIdentity is one HTTP-Basic-auth grant in a Policy. PasswordBcrypt stores the bcrypt-hashed form of the operator's chosen password (`bcrypt.GenerateFromPassword` at cost ≥ 10); raw passwords NEVER appear in the config file or in process memory beyond the per- request verification step.

Username is the wire identifier (sent client-side in `Authorization: Basic <base64(username:password)>`); ID is the audit identifier that shows up in Identity.ID and downstream logs. They MAY be the same string but are kept distinct so operators can rename machine-facing usernames without rewriting log queries.

Threat note: bcrypt verification runs on every request that presents a Basic header. This is intentionally CPU-bound (default cost 10 ≈ 60ms on contemporary hardware). A malicious actor with a stream of wrong passwords can therefore burn server CPU; mitigate via a fronting rate-limiter or an LB-level connection cap. The auth layer does NOT itself rate-limit (see RFC 0003 open question 3 for the trade-offs).

type CertIdentity

type CertIdentity struct {
	SubjectCN string
	Scopes    []Scope
}

CertIdentity is one mTLS-cert-based grant in a Policy. SubjectCN is matched against tls.ConnectionState.VerifiedChains[0][0].Subject.CommonName of the peer certificate. Exact-match only — wildcard CN matching invites accidental over-grant and is deferred until a concrete operator request justifies the complexity.

type Identity

type Identity struct {
	ID     string
	Scopes []Scope
}

Identity is the resolved caller for an authorized request. Stored into fiber.Ctx locals under IdentityKey so handlers can attribute audit logs / metrics to the calling principal without re-deriving it. ID is the human-readable label the operator put in the auth config; Scopes is the union of grants for that principal.

func (Identity) Capabilities added in v0.8.3

func (i Identity) Capabilities() []string

Capabilities returns the stable capability strings derived from the identity's scopes. Capabilities are the surface clients introspect via GET /v1/me — they describe what the caller can DO rather than what scopes they HAVE. The two are 1:1 today (one capability per scope, prefixed with `cache.`) but the indirection lets us split a scope into multiple capabilities later (e.g. ScopeRead → cache.read + cache.metrics) without breaking clients that key off capability strings.

func (Identity) HasCapability added in v0.8.4

func (i Identity) HasCapability(name string) bool

HasCapability reports whether the identity carries the given capability string. Used by the /v1/me/can probe endpoint and by the client SDK's mirror method — both want a single authoritative check rather than reinventing the scope-prefix-to-capability mapping at each call site.

func (Identity) HasScope

func (i Identity) HasScope(s Scope) bool

HasScope reports whether the identity carries the given scope. O(n) over the identity's scope list — n is small (3 max), so micro-optimizations like a bitmap are not justified.

type Policy

type Policy struct {
	// Tokens are the bearer-token identities. Constant-time
	// compared against the Authorization header.
	Tokens []TokenIdentity
	// BasicIdentities are the HTTP-Basic-auth identities.
	// Verified by bcrypt-comparing the password presented in
	// `Authorization: Basic ...` against PasswordBcrypt.
	BasicIdentities []BasicIdentity
	// CertIdentities are the mTLS-cert identities. Resolved
	// from the verified peer cert when TLS is enabled with
	// client-cert verification.
	CertIdentities []CertIdentity
	// ServerVerify (optional) is the universal escape hatch.
	// When set and bearer + cert both miss, the hook is called
	// last; returning a non-error Identity authorizes the
	// request. Use for JWT, OIDC introspection, or any other
	// auth scheme this package doesn't natively support.
	ServerVerify func(fiber.Ctx) (Identity, error)
	// AllowAnonymous permits requests with no credentials at
	// all to pass — they get the empty Identity with no
	// scopes, and only routes requiring no specific scope
	// will accept them. Defaults to false. Used by tests and
	// dev-mode deployments; production should always require
	// at least one credential class.
	AllowAnonymous bool
	// AllowBasicWithoutTLS lets Basic auth verify even when the
	// connection is plaintext. Defaults to false (fails closed:
	// Basic over plaintext leaks the password to every network
	// observer). Operators set this to true ONLY for local dev
	// stacks where TLS termination happens elsewhere or is
	// intentionally skipped. Production must leave this false.
	AllowBasicWithoutTLS bool
}

Policy is the authoritative auth configuration for an HTTP listener. Build via the loader in this package or construct in-process for tests; pass the same value to every route via Middleware.

Policy is value-semantic and safe for concurrent use after construction — the slices are read-only after load, the ServerVerify hook is the operator's responsibility to make goroutine-safe.

func LoadFromEnv

func LoadFromEnv() (Policy, error)

LoadFromEnv resolves a client-API auth Policy from the process environment. Precedence:

  1. EnvAuthConfig set → load multi-token + cert-identity Policy from the YAML file. Missing or malformed file returns an error; the caller exits non-zero. This is a behavioral break vs the legacy "missing token = open mode" posture and is documented in CHANGELOG.
  2. EnvAuthToken set → synthesize a single TokenIdentity with all three scopes. Mirrors the pre-v2 behavior where one token gated every protected route, so existing zero-config deployments keep working byte-identical.
  3. Neither set → return the zero Policy. Caller should log a "running with no auth" warning and decide whether to opt into AllowAnonymous mode.

EnvAuthConfig and EnvAuthToken are NOT mutually exclusive: the dist transport's symmetric peer auth still reads EnvAuthToken directly (see cmd/hypercache-server/main.go's buildHyperCache). When both are set, EnvAuthConfig wins for the client API and EnvAuthToken is reused for dist — this is the standard config for a multi-tenant client API on top of a single-trust-domain cluster.

LoadFromEnv runs Policy.Validate before returning so the caller gets a single error path; misconfigured policies surface as errors here rather than as silent runtime auth bypasses.

func (Policy) IsConfigured

func (p Policy) IsConfigured() bool

IsConfigured reports whether the policy has at least one credential class configured. The zero Policy (no tokens, no certs, no ServerVerify, AllowAnonymous false) maps to "auth disabled" mode — every request passes through. Loaders and the hypercache-server binary use this to decide whether to emit a "running with no auth" startup warning. Callers should NOT use it to gate security checks — Middleware already handles the no-credentials-configured fall-through correctly.

func (Policy) Middleware

func (p Policy) Middleware(required Scope) fiber.Handler

Middleware returns a fiber middleware that enforces the policy for the given required scope. Order of credential resolution:

  1. Bearer token in Authorization header — constant-time compared against EVERY configured token even on early match, so the count of configured tokens does not leak via timing.
  2. mTLS verified peer cert (if TLSConnectionState present and VerifiedChains is non-empty) — Subject CN matched against CertIdentities.
  3. ServerVerify hook (if non-nil) — last-resort escape hatch.

On any successful match the resolved Identity is stored under IdentityKey and the next handler runs. On no match the request gets 401 Unauthorized with no body — credential-class hints (which class missed) are deliberately omitted to avoid handing attackers a credential-discovery oracle.

When the policy has no configured credentials AND the route requires a scope, every request fails 401 — this is fail-closed by design. Operators in dev mode should set AllowAnonymous=true to opt into permissive behavior.

When the route requires no specific scope (the empty string is passed as `required`), the middleware skips scope-checking but still resolves the Identity for handlers that want to attribute the call. This shape is currently unused but reserved for routes that want any-authenticated-caller semantics.

func (Policy) Validate

func (p Policy) Validate() error

Validate enforces coherence at load time. Returns nil for the zero Policy (open mode by virtue of nothing being configured) and for any policy with at least one credential class. The AllowAnonymous-with-no-credentials shape is intentionally permitted: it's how the hypercache-server binary preserves the pre-v2 zero-config dev posture (no env vars set → open mode).

validate's failure modes are all caller-error rather than runtime-error. Loaders should call this once at startup and exit non-zero on failure, never silently continue.

func (Policy) Verify added in v0.7.0

func (p Policy) Verify(c fiber.Ctx, required Scope) error

Verify resolves credentials, asserts the required scope, and stores the resolved Identity in c.Locals(IdentityKey). Returns nil on success; on failure returns a *fiber.Error carrying status 401 (no credentials matched) or 403 (credentials matched but scope is missing). Fiber's default error handler emits the canonical text body for the status code.

Use Verify when integrating with code that owns its own next- handler dispatch — e.g. ManagementHTTPServer.WithMgmtAuth and WithMgmtControlAuth, which short-circuit on a non-nil return from the gate function and never call the wrapped handler. Middleware() is thin sugar over Verify() + Next() so the auth logic lives in exactly one place.

CRITICAL: do NOT switch to `c.SendStatus(...)` here. SendStatus returns nil on success, which would silently fall through to the wrapped handler in wrapWithGate-style adapters and the downstream handler would write its own success status over the 401 body. Returning a *fiber.Error keeps both Middleware and the gate adapters fail-closed.

type Scope

type Scope string

Scope is a coarse permission grant applied to an identity. The three-scope model maps cleanly to cache semantics: Read covers GET/HEAD/owners-lookup; Write covers PUT/DELETE plus their batch equivalents; Admin is reserved for management-plane endpoints (not yet wired — see plan §"Out of scope: Unifying management HTTP").

Scopes are inclusive, not hierarchical: an identity granted Write does NOT implicitly also have Read. Each route declares the exact scope it requires; identities carry the union of scopes they hold. Hierarchical inheritance (admin > write > read) was rejected as the wrong default — it makes "read-only" tokens impossible without inverting the polarity, and operators routinely want a service that can write but not read (think: ingest-only metrics writers).

const (
	// ScopeRead permits cache lookups and metadata inspection.
	ScopeRead Scope = "read"
	// ScopeWrite permits cache mutations.
	ScopeWrite Scope = "write"
	// ScopeAdmin permits management-plane operations (cluster
	// control, eviction triggers, etc.). Unused by the client
	// API today; reserved for the management HTTP unification.
	ScopeAdmin Scope = "admin"
)

type TokenIdentity

type TokenIdentity struct {
	ID     string
	Token  string
	Scopes []Scope
}

TokenIdentity is one bearer-token grant in a Policy. The Token field is the raw secret; never log it. ID is what shows up in audit logs / Identity.ID after a successful match.

Jump to

Keyboard shortcuts

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