middleware

package
v0.40.7 Latest Latest
Warning

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

Go to latest
Published: May 28, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package middleware provides HTTP middleware primitives for the SpeechKit server adapter. Each middleware is a stand-alone decorator so tests can compose them individually.

Index

Constants

View Source
const (
	AdminCSRFCookieName = adminCSRFCookieName
	AdminCSRFHeaderName = adminCSRFHeaderName
)

AdminCSRFCookieName and AdminCSRFHeaderName are exported so server integration tests and the SPA build pipeline can reference the double-submit contract without re-declaring it.

View Source
const AdminSessionCookieName = adminSessionCookieName

AdminSessionCookieName is exported for server integration tests that verify setup/auth transitions without duplicating the cookie contract.

Variables

This section is empty.

Functions

func AdminPasswordMatches added in v0.33.0

func AdminPasswordMatches(username, passwordHash, presentedUser, presentedPassword string) bool

func Chain

func Chain(mws ...Middleware) func(http.Handler) http.Handler

Chain composes middlewares left-to-right: the first argument is the outermost wrapper (runs first on request, last on response).

func EnforceAdminCSRF added in v0.37.8

func EnforceAdminCSRF(w http.ResponseWriter, r *http.Request) bool

EnforceAdminCSRF is the handler-level helper for endpoints that accept admin-session-cookie-authenticated state changes. Call it at the top of the handler when the resolved Identity has Source="admin_session"; the helper writes 403 + JSON envelope when the double-submit check fails and returns false (caller MUST return without further writes).

Bearer / edge-HMAC / smoke callers are not subject to CSRF (their credentials are not automatically attached by the browser to cross-origin requests), so they should skip this check entirely.

func InjectIdentityForTest added in v0.37.8

func InjectIdentityForTest(ctx context.Context, id Identity) context.Context

InjectIdentityForTest attaches an Identity to the context using the same unexported key the Auth middleware uses. Exported solely so handler tests in external packages can exercise endpoints that depend on IdentityFromContext without spinning up the full auth middleware. Production code MUST NOT use this; the function name and the package it lives in are intentionally awkward to make accidental use loud at review time.

func NewAdminCSRFCookie added in v0.37.8

func NewAdminCSRFCookie(sessionCookieValue string, secure bool, now time.Time) *http.Cookie

NewAdminCSRFCookie returns the JS-readable double-submit CSRF cookie that pairs with the admin session cookie minted by NewAdminSessionCookie. Callers that issue the session cookie MUST also issue this cookie in the same response — otherwise the SPA has no token to echo in X-CSRF-Token and EnforceAdminCSRF will refuse the next state-changing request from the admin_session identity.

sessionCookieValue is the Value of the session cookie returned by NewAdminSessionCookie. The returned CSRF cookie carries csrfTokenFor(sessionCookieValue) so the binding HMAC over the session signature also covers this cookie.

func NewAdminSessionCookie added in v0.33.0

func NewAdminSessionCookie(username, passwordHash string, secure bool, now time.Time) (*http.Cookie, error)

func ValidateAdminCSRF added in v0.37.8

func ValidateAdminCSRF(r *http.Request) bool

ValidateAdminCSRF returns true when the request carries a CSRF cookie + matching X-CSRF-Token header AND the cookie value matches csrfTokenFor(<session-cookie>). All comparisons are constant-time. Returns false when any input is missing or mismatched — callers should reject the request with 403.

Types

type AuthMode

type AuthMode string

AuthMode selects which credential format the server accepts.

const (
	// AuthModeNone disables built-in server authentication. The request still
	// receives a stable anonymous identity so mode handlers can apply session
	// ownership and rate-limit logic without requiring an upstream auth layer.
	AuthModeNone AuthMode = "none"
	// AuthModeBearer requires a static bearer token from the configured env
	// var. Minimum viable auth; suitable for same-network service-to-service
	// calls (e.g. kombify-AI → speechkit over Render private network).
	AuthModeBearer AuthMode = "bearer"
	// AuthModeEdgeHMAC trusts HMAC-signed headers from a known edge
	// (Cloudflare Worker / reverse proxy). The actual user identity comes
	// from the edge. Expected header set:
	//   X-Edge-Auth-Hmac, X-Edge-User-Id, X-Edge-Org-Id, X-Edge-Plan,
	//   and optional X-Edge-Role. Role is covered by the HMAC when present.
	AuthModeEdgeHMAC AuthMode = "edge_hmac"
	// AuthModeBearerOrEdge accepts either credential format; handy when a
	// single deployment serves both internal services (bearer) and
	// browser-originated traffic (edge-signed).
	AuthModeBearerOrEdge AuthMode = "bearer_or_edge"
)

type AuthOptions

type AuthOptions struct {
	Mode              string
	BearerTokenEnv    string
	EdgeSecretEnv     string
	BearerRole        string
	AllowPublicPaths  []string // exact path matches that skip auth entirely (e.g. /healthz)
	AllowPublicRoutes []PublicRoute
	// HTMLUnauthorizedPaths/Routes keep browser-facing admin UI failures out
	// of the JSON API envelope while still requiring normal credentials.
	HTMLUnauthorizedPaths  []string
	HTMLUnauthorizedRoutes []PublicRoute
	// Dynamic providers are evaluated for every request. They let first-run
	// setup generate a token without rebuilding the middleware chain.
	ModeProvider              func() string
	BearerTokenProvider       func() string
	EdgeSecretProvider        func() string
	BearerRoleProvider        func() string
	AdminUsernameProvider     func() string
	AdminPasswordHashProvider func() string
	// SmokeTokenProvider returns the optional public demo token used by the
	// smoke UI on `/`. When non-empty and matching the presented Bearer,
	// the middleware attaches a Source="smoke", Plan="demo" identity so
	// handlers and the rate-limiter can treat demo traffic accordingly.
	SmokeTokenProvider func() string
	// Bootstrap routes are public only while BootstrapAllowed returns true.
	// The server uses this for the first settings write when bearer auth is
	// configured but no bearer token exists yet.
	AllowBootstrapPaths  []string
	AllowBootstrapRoutes []PublicRoute
	BootstrapAllowed     func(*http.Request) bool
	// RequireAuthenticatedMode is defence-in-depth on top of
	// config.ValidateServerProductionAuth. When true, the AuthModeNone
	// branch of verify() refuses to issue the anonymous Identity even if
	// the resolved mode is "none" or empty. Bootstrap sets this for
	// non-loopback binds so a future code path that skips startup
	// validation cannot accidentally serve unauthenticated traffic to the
	// public internet. Admin-session and smoke-token fallbacks remain
	// available — only the implicit anonymous identity is suppressed.
	RequireAuthenticatedMode bool
}

AuthOptions configures the Auth middleware.

type AuthState added in v0.28.2

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

AuthState is a concurrency-safe view of the mutable auth config used by the server setup flow. It stores env var names, not secret values.

func NewAuthState added in v0.28.2

func NewAuthState(mode, bearerTokenEnv, edgeSecretEnv, adminUsername, adminPasswordHash string) *AuthState

func (*AuthState) AdminPasswordHash added in v0.31.0

func (s *AuthState) AdminPasswordHash() string

func (*AuthState) AdminUsername added in v0.31.0

func (s *AuthState) AdminUsername() string

func (*AuthState) BearerToken added in v0.28.2

func (s *AuthState) BearerToken() string

func (*AuthState) BearerTokenEnv added in v0.28.2

func (s *AuthState) BearerTokenEnv() string

func (*AuthState) EdgeSecret added in v0.28.2

func (s *AuthState) EdgeSecret() string

func (*AuthState) Mode added in v0.28.2

func (s *AuthState) Mode() string

func (*AuthState) Set added in v0.28.2

func (s *AuthState) Set(mode, bearerTokenEnv, edgeSecretEnv string)

func (*AuthState) SetAdmin added in v0.31.0

func (s *AuthState) SetAdmin(username, passwordHash string)

func (*AuthState) SetSmokeTokenEnv added in v0.35.3

func (s *AuthState) SetSmokeTokenEnv(envName string)

SetSmokeTokenEnv records the env var name holding the optional public demo token. Empty string disables smoke-from-page authentication.

func (*AuthState) SmokeToken added in v0.35.3

func (s *AuthState) SmokeToken() string

SmokeToken resolves the current value of the demo bearer token, or "" if the env var is unset / unconfigured.

func (*AuthState) SmokeTokenEnv added in v0.35.3

func (s *AuthState) SmokeTokenEnv() string

SmokeTokenEnv returns the configured env var name for the demo token.

type Identity

type Identity struct {
	UserID string `json:"user_id"`
	OrgID  string `json:"org_id"`
	Plan   string `json:"plan"`
	Role   string `json:"role,omitempty"` // "admin" | "" (default)
	Source string `json:"source"`         // "none" | "bearer" | "edge_hmac" | "basic" | "admin_session"
}

Identity is attached to the request context by Auth and consumed by mode handlers for rate-limit keying, session ownership, and audit logs.

func IdentityFromContext

func IdentityFromContext(ctx context.Context) Identity

IdentityFromContext returns the Identity attached by Auth, or the zero Identity if none is present.

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware is the standard HTTP decorator signature.

func Auth

func Auth(opts AuthOptions) Middleware

Auth validates credentials according to the configured mode and attaches the resolved Identity to the request context. Unauthenticated requests receive 401 with a JSON error envelope.

func CORS

func CORS(allowedOrigins []string) Middleware

CORS returns a middleware that handles preflight and attaches CORS response headers when the request Origin matches one of the allowed entries.

Empty allowedOrigins disables CORS entirely (safe default; internal service-to-service calls never trigger a browser preflight anyway). Special-case "*" opens CORS to all origins — use with caution and only for OSS dev mode, never when the server is behind auth that trusts cookies.

func Logging

func Logging() Middleware

Logging emits a structured slog record per request including method, path, status, bytes written, and wall-clock duration. Kept dependency-free so the adapter stays transportable to any Go runtime that ships slog.

func RateLimit

func RateLimit(opts RateLimitOptions) Middleware

RateLimit returns a middleware that enforces a per-identity token bucket. Identities come from the Auth middleware; requests without an identity key on the remote address, which is rough but adequate for v1. For production behind a trusted LB, swap to a distributed limiter (Redis, envoy).

Keeping it in-memory is a deliberate v1 choice — it adds zero external dependencies to the OSS Server-Target. The map is bounded by MaxBuckets with LRU eviction so a flood of distinct identities cannot exhaust memory.

func Recover

func Recover() Middleware

Recover turns panics in downstream handlers into 500 responses with a JSON error envelope, logging the stack trace for post-mortem debugging. Without this, a single buggy handler crashes the whole process.

type PublicRoute added in v0.28.0

type PublicRoute struct {
	Path       string
	PathPrefix string
	PathSuffix string
	Methods    []string
}

type RateLimitOptions

type RateLimitOptions struct {
	RequestsPerSecond float64 // sustained rate; zero disables limiting
	Burst             int     // max tokens in bucket; zero disables limiting
	// AllowPublicPaths is the list of exact request paths that bypass the
	// limiter entirely. Production deployments must always include
	// `/healthz` and `/readyz` so external probes (Render, Kubernetes) are
	// never rate-limited away from a real outage.
	AllowPublicPaths []string
	// MaxBuckets caps the in-memory bucket map. Zero falls back to
	// defaultRateLimitMaxBuckets. Once the cap is hit, the least-recently-
	// used bucket is evicted before a new one is inserted.
	MaxBuckets int
	// SweepInterval controls how often a background goroutine scans the
	// bucket map and evicts entries whose last access is older than
	// SweepMaxAge. Zero falls back to 5 minutes; set negative to disable.
	SweepInterval time.Duration
	// SweepMaxAge is the staleness threshold beyond which buckets are
	// evicted by the background sweeper. Zero falls back to a value
	// derived from Burst/RequestsPerSecond.
	SweepMaxAge time.Duration
	// Context, when non-nil, controls the lifetime of the background
	// sweeper goroutine. Cancellation stops the sweep loop. Production
	// callers should pass the server's shutdown context here.
	Context context.Context //nolint:containedctx // intentional middleware-lifetime ctx
	// EndpointCosts assigns a per-endpoint token cost so expensive
	// handlers (LLM, transcription, session create) drain the bucket
	// faster than cheap ones. Lookup is "METHOD PATH" first (e.g.
	// "POST /v1/dictation/transcribe"), then bare PATH, then default
	// 1.0. Zero or negative entries fall back to 1.0. (Audit S-4)
	EndpointCosts map[string]float64
	// DemoDailyQuota caps how many requests an identity with
	// Plan="demo" (the smoke-token surface) may make per UTC day,
	// keyed by UserID + client IP. Zero disables the quota. Public
	// paths bypass this check the same way they bypass token-bucket
	// rate limiting. (Audit S-5)
	DemoDailyQuota int
	// Now lets tests inject a fake clock for the daily-quota window
	// reset; production leaves it nil and gets time.Now.
	Now func() time.Time
}

RateLimitOptions configures the in-memory token-bucket limiter.

Jump to

Keyboard shortcuts

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