httpauth

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: May 10, 2026 License: MPL-2.0 Imports: 9 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 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) 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
	// 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
}

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