authn

package
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package authn holds the transport-independent authentication primitives shared by the HTTP, WebSocket, and gRPC surfaces: the tenant key store and the authenticated principal that travels on a request context.

The package deliberately knows nothing about net/http or gRPC — each transport adapts its own credential carrier (Authorization header, Sec-WebSocket-Protocol entry, gRPC metadata) into the same Principal.

Index

Constants

View Source
const (
	ReasonMissingCredential = "missing_header"
	ReasonBadScheme         = "bad_scheme"
	ReasonBadKey            = "bad_key"
	ReasonBadOrigin         = "bad_origin"
	ReasonBadTenant         = "bad_tenant"
)

Failure reasons reported to the auth-failure metric. They are stable strings: "missing_header", "bad_scheme", and "bad_key" predate this package and are kept byte-identical so existing dashboards keep working.

View Source
const (
	// WSSubprotocol is the only WebSocket subprotocol the server echoes. A
	// browser that must carry a credential offers it alongside an
	// `auth.<base64url-token>` entry; the server selects this one, so the token
	// never appears in the negotiated protocol or in a log line.
	WSSubprotocol = "otelcontext.v1"

	// WSAuthProtoPrefix marks the credential-bearing subprotocol entry.
	WSAuthProtoPrefix = "auth."
)
View Source
const BearerPrefix = "Bearer "

BearerPrefix is the only credential scheme accepted on every surface.

View Source
const MaxKeyFileBytes = 1 << 20

MaxKeyFileBytes bounds how much of an operator-supplied key file is read. 1 MiB is ~20k entries; anything larger is a misconfiguration, not a deployment.

Variables

View Source
var ConflictHook func(surface, reason string)

ConflictHook is called when a bound principal ignores a client-asserted tenant. surface is "http", "ws", or "grpc"; reason names the ignored carrier ("header", "metadata", "resource_attribute"). Wired by main.go to a Prometheus counter; safe to leave nil. Never called with credential material — only the surface and the carrier name.

Functions

func BoundTenantFromContext

func BoundTenantFromContext(ctx context.Context) (string, bool)

BoundTenantFromContext returns the tenant a bound principal pinned onto ctx. Operator and unauthenticated contexts return ("", false), which leaves the caller's existing tenant precedence untouched.

func RecordConflict

func RecordConflict(surface, reason string)

RecordConflict reports an ignored tenant assertion. No-op when unset.

func TokenFromAuthorization

func TokenFromAuthorization(header string) (string, string)

TokenFromAuthorization extracts the bearer token from an Authorization header value. The second result is a failure reason when no usable token is present; callers that have a second credential carrier (WebSocket subprotocol, gRPC metadata) may ignore it and try that instead.

func WithPrincipal

func WithPrincipal(ctx context.Context, p Principal) context.Context

WithPrincipal returns a copy of ctx carrying the authenticated principal.

Types

type Authenticator

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

Authenticator resolves a bearer token into a Principal. It is shared by the HTTP, WebSocket, and gRPC adapters so all three surfaces agree on what a credential means; only credential extraction differs per transport.

A zero-value / disabled Authenticator authenticates nothing and reports Enabled() == false, which is how a development deployment with no API_KEY, no tenant keys file, and no external trust keeps today's open behaviour.

func NewAuthenticator

func NewAuthenticator(operatorKey string, store *KeyStore, trustExternal bool) *Authenticator

NewAuthenticator wires the operator key (API_KEY), the per-tenant key store (API_TENANT_KEYS_FILE), and the AUTH_TRUST_EXTERNAL switch.

func (*Authenticator) AuthenticateToken

func (a *Authenticator) AuthenticateToken(token string) (Principal, string, bool)

AuthenticateToken resolves a raw bearer token. The operator key is checked first with a constant-time compare, then the tenant key store. A miss returns ReasonBadKey; an empty token returns ReasonMissingCredential.

func (*Authenticator) Enabled

func (a *Authenticator) Enabled() bool

Enabled reports whether any credential source is configured. When false the caller must not gate anything: authentication arrives with configuration, never by surprise.

func (*Authenticator) ExternalPrincipal

func (a *Authenticator) ExternalPrincipal(raw string) (Principal, bool)

ExternalPrincipal converts a proxy-injected tenant value into a bound principal. It returns false unless AUTH_TRUST_EXTERNAL is on and the value survives the shared tenant sanitizer.

The injected header is trusted ONLY because the operator asserted that a front proxy authenticates the caller, strips inbound copies of this header, and makes the application ports unreachable except through it. Without those conditions this is an authentication bypass — see CLAUDE.md.

func (*Authenticator) HasTenantKeys

func (a *Authenticator) HasTenantKeys() bool

HasTenantKeys reports whether per-tenant keys are configured.

func (*Authenticator) TenantKeyCount

func (a *Authenticator) TenantKeyCount() int

TenantKeyCount is the number of configured tenant keys (never their values).

func (*Authenticator) Tenants

func (a *Authenticator) Tenants() []string

Tenants lists the tenants covered by the key store.

func (*Authenticator) TrustExternal

func (a *Authenticator) TrustExternal() bool

TrustExternal reports whether a proxy-injected identity header is honoured.

type KeyStore

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

KeyStore maps bearer keys to tenants using digest-only storage and constant-time comparison. A nil or empty store is disabled and every Lookup misses, which is what keeps the default (no keys file) deployment on the legacy shared-API_KEY path.

func LoadKeyStore

func LoadKeyStore(path string) (*KeyStore, error)

LoadKeyStore reads API_TENANT_KEYS_FILE. Format is chosen by extension: `.json` for JSON, `.yaml`/`.yml` for YAML. Both carry the same shape — an object mapping bearer key to tenant ID:

{"3f7c…": "acme", "9b21…": "acme", "c40d…": "beta"}

Multiple keys may map to the same tenant (rotation, per-agent keys). The load is startup-only; there is no reload path by design, so a key file swap is an explicit restart.

Refused: unreadable files, files readable or writable by group/other, unknown extensions, empty files, empty keys, keys carrying whitespace or control characters, tenant IDs the storage sanitizer rejects, and duplicate keys. Errors never quote key material.

func NewKeyStoreFromMap

func NewKeyStoreFromMap(m map[string]string) (*KeyStore, error)

NewKeyStoreFromMap builds a store from an in-memory mapping. Iteration order of a Go map is random, so entries are sorted by tenant then key digest to keep error messages deterministic.

func (*KeyStore) Enabled

func (s *KeyStore) Enabled() bool

Enabled reports whether any tenant key is configured.

func (*KeyStore) Len

func (s *KeyStore) Len() int

Len is the number of configured keys.

func (*KeyStore) Lookup

func (s *KeyStore) Lookup(key string) (string, bool)

Lookup returns the tenant bound to key. The presented key is digested and compared against every stored digest with subtle.ConstantTimeCompare, and the scan never exits early, so neither the match position nor a partial prefix match is observable through timing.

func (*KeyStore) Tenants

func (s *KeyStore) Tenants() []string

Tenants returns the sorted, unique tenants covered by the store. Safe to log: tenant IDs are not credentials.

type Kind

type Kind string

Kind classifies an authenticated credential.

const (
	// KindOperator is the shared API_KEY. It is authorized for every tenant,
	// so tenant selection keeps its historical precedence (explicit header or
	// metadata → trusted resource attribute → DEFAULT_TENANT).
	KindOperator Kind = "operator"

	// KindTenant is a key from API_TENANT_KEYS_FILE. Its tenant binding is
	// absolute: client-asserted tenant headers, gRPC metadata, and OTLP
	// `tenant.id` resource attributes are ignored (and counted) for the
	// lifetime of the request or connection.
	KindTenant Kind = "tenant"

	// KindExternal is an identity injected by a front proxy and trusted only
	// because AUTH_TRUST_EXTERNAL=true. It binds like KindTenant.
	KindExternal Kind = "external"
)

type Principal

type Principal struct {
	Kind   Kind
	Tenant string
}

Principal is the authenticated identity of a request or connection. The zero value means "unauthenticated" — which is the normal state of a development deployment with no API_KEY and no tenant keys file.

func PrincipalFromContext

func PrincipalFromContext(ctx context.Context) (Principal, bool)

PrincipalFromContext returns the principal stashed by WithPrincipal. The second result is false when the request was never authenticated.

func (Principal) Authenticated

func (p Principal) Authenticated() bool

Authenticated reports whether a credential was presented and accepted.

func (Principal) Bound

func (p Principal) Bound() bool

Bound reports whether the principal pins the tenant irrevocably. Operator principals are never bound: they may address any tenant.

Jump to

Keyboard shortcuts

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