auth

package
v0.12.1 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package auth provides authentication and RBAC for ChronosOS.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func APIKeyMiddleware added in v0.3.0

func APIKeyMiddleware(cfg APIKeyConfig) func(http.Handler) http.Handler

APIKeyMiddleware returns HTTP middleware that validates API keys from the configured header (default: X-Api-Key).

Keys are matched against a hashed KeyStore using constant-time comparison; raw key material is never retained in the middleware. When Quotas is set, per-key and per-tenant limits are enforced (HTTP 429 on breach).

func CreateTestToken added in v0.3.0

func CreateTestToken(claims UserClaims, secret string) string

CreateTestToken creates a simple HMAC-SHA256 signed JWT for testing.

func HashKey added in v0.5.0

func HashKey(rawKey string) string

HashKey returns the hex-encoded SHA-256 digest of a raw API key. SHA-256 is appropriate here because API keys are high-entropy secrets (unlike low-entropy passwords, which would warrant bcrypt/argon2).

func JWTMiddleware added in v0.3.0

func JWTMiddleware(cfg JWTConfig) func(http.Handler) http.Handler

JWTMiddleware returns HTTP middleware that validates JWT bearer tokens.

func RequireRole added in v0.3.0

func RequireRole(svc *Service, required Role) func(http.Handler) http.Handler

RequireRole returns middleware that checks the authenticated user has at least the specified role. Must be used after JWT or API key middleware.

func WithUser added in v0.3.0

func WithUser(ctx context.Context, claims *UserClaims) context.Context

WithUser adds UserClaims to the context.

Types

type APIKeyConfig added in v0.3.0

type APIKeyConfig struct {
	// HeaderName is the request header carrying the key (default: X-Api-Key).
	HeaderName string
	// Keys is the legacy inline key map. Raw keys are hashed at construction.
	Keys map[string]APIKeyEntry
	// Store is an optional persisted, hashed key store. When set it is used
	// instead of Keys.
	Store KeyStore
	// SkipPaths bypass authentication entirely.
	SkipPaths []string

	// Quotas, when set, enforces per-key and per-tenant quotas on each request.
	Quotas QuotaStore
	// TenantQuotas maps a tenant ID to its quota, enforced when Quotas is set.
	TenantQuotas map[string]Quota
}

APIKeyConfig holds configuration for API key authentication.

Backward compatibility: the legacy Keys map (raw key -> entry) is still accepted. At middleware construction the raw keys are hashed into an in-memory KeyStore and the plaintext is not retained. For a persisted, pre-hashed store, set Store instead (it takes precedence over Keys).

type APIKeyEntry added in v0.3.0

type APIKeyEntry struct {
	Scope    string
	UserID   string
	TenantID string
	Quota    Quota
}

APIKeyEntry represents a configured API key with associated metadata.

type Audience added in v0.5.0

type Audience []string

Audience represents a JWT "aud" claim which may be encoded either as a single string or as an array of strings. It normalizes both forms to a slice.

func (Audience) Contains added in v0.5.0

func (a Audience) Contains(v string) bool

Contains reports whether the audience set includes the given value.

func (Audience) MarshalJSON added in v0.5.0

func (a Audience) MarshalJSON() ([]byte, error)

MarshalJSON emits a single string when there is exactly one audience, otherwise a JSON array.

func (*Audience) UnmarshalJSON added in v0.5.0

func (a *Audience) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts both a JSON string and a JSON array of strings.

type JWTConfig added in v0.3.0

type JWTConfig struct {
	// Secret is the shared HMAC secret for HS256 tokens. Required to accept
	// HS256 tokens; leave empty to reject them.
	Secret string
	// Issuer, when set, is enforced against the token "iss" claim.
	Issuer string
	// Audience, when set, is enforced against the token "aud" claim.
	Audience string
	// SkipPaths are request paths that bypass authentication entirely.
	SkipPaths []string

	// AllowExpiredAt is the grace window during which an already-expired token
	// is still accepted, but ONLY on paths listed in AllowExpiredPaths (i.e.
	// explicit token-refresh endpoints). It has no effect elsewhere.
	AllowExpiredAt time.Duration
	// AllowExpiredPaths enumerates the refresh-flow paths on which
	// AllowExpiredAt applies.
	AllowExpiredPaths []string

	// RSAPublicKey is a static RSA public key for verifying RS256/384/512
	// tokens without JWKS. Mutually usable with JWKSURL (JWKS takes precedence
	// when a matching kid is found).
	RSAPublicKey *rsa.PublicKey
	// JWKSURL is an OIDC/JWKS endpoint. Keys are fetched, cached by "kid", and
	// re-fetched on cache miss (rotation) or expiry.
	JWKSURL string
	// JWKSCacheTTL bounds how long JWKS entries are cached before a refresh.
	// Defaults to defaultJWKSCacheTTL when zero.
	JWKSCacheTTL time.Duration
	// HTTPClient is used for JWKS fetches; defaults to a client with a short
	// timeout when nil.
	HTTPClient *http.Client

	// AllowNoExpiry permits tokens that omit the "exp" claim. It defaults to
	// false: a token with no expiry is rejected, so a signed token can never
	// become a permanent, unrevocable credential. Set true only for trusted
	// issuers that deliberately mint non-expiring service tokens.
	AllowNoExpiry bool
	// contains filtered or unexported fields
}

JWTConfig holds configuration for JWT authentication.

Backward compatibility: an HS256 deployment only needs Secret (and optionally Issuer/Audience/SkipPaths). RS256/OIDC deployments set either RSAPublicKey (a static key) or JWKSURL (dynamic keys fetched and cached by "kid", honoring rotation). The verification path is selected per-token from the JWS "alg" header, so both HS256 and RS256 tokens can be accepted by the same server if both a Secret and an RSA key source are configured.

type KeyRecord added in v0.5.0

type KeyRecord struct {
	// ID is a stable, non-secret identifier/label for the key.
	ID string `json:"id"`
	// Hash is the hex-encoded SHA-256 of the raw key.
	Hash string `json:"hash"`
	// Scope is the RBAC scope/role granted to callers using this key.
	Scope string `json:"scope"`
	// UserID identifies the principal the key acts as.
	UserID string `json:"user_id"`
	// TenantID identifies the tenant for per-tenant quota accounting.
	TenantID string `json:"tenant_id,omitempty"`
	// Quota, when non-zero, is enforced per key on each request.
	Quota Quota `json:"quota,omitempty"`
	// Disabled revokes the key without deleting its record.
	Disabled bool `json:"disabled,omitempty"`
	// CreatedAt records when the key was added.
	CreatedAt time.Time `json:"created_at"`
}

KeyRecord is the persisted metadata for an API key. The raw key is never stored; only its hash (see HashKey) is retained.

type KeyStore added in v0.5.0

type KeyStore interface {
	// Lookup resolves the raw key to its record using constant-time comparison.
	// It returns (record, true, nil) on match, (nil, false, nil) when no active
	// key matches, and a non-nil error only on backend failure.
	Lookup(ctx context.Context, rawKey string) (*KeyRecord, bool, error)
	// Add hashes rawKey and stores the record; the returned record has Hash and
	// CreatedAt populated. The raw key is not retained.
	Add(ctx context.Context, rawKey string, meta KeyRecord) (KeyRecord, error)
	// Remove deletes the record with the given ID.
	Remove(ctx context.Context, id string) error
	// List returns all stored records (without raw keys).
	List(ctx context.Context) ([]KeyRecord, error)
}

KeyStore is a persisted, hashed API-key store. Implementations must never retain the raw key material and must compare using constant-time operations. The control plane can wire a database-backed implementation; MemoryKeyStore is the default.

type MemoryKeyStore added in v0.5.0

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

MemoryKeyStore is an in-memory KeyStore keyed by record ID. It is safe for concurrent use.

func NewMemoryKeyStore added in v0.5.0

func NewMemoryKeyStore() *MemoryKeyStore

NewMemoryKeyStore returns an empty in-memory key store.

func (*MemoryKeyStore) Add added in v0.5.0

func (s *MemoryKeyStore) Add(_ context.Context, rawKey string, meta KeyRecord) (KeyRecord, error)

Add hashes and stores the key. If meta.ID is empty, the key hash is used as the ID.

func (*MemoryKeyStore) List added in v0.5.0

func (s *MemoryKeyStore) List(_ context.Context) ([]KeyRecord, error)

List returns a snapshot of all records.

func (*MemoryKeyStore) Lookup added in v0.5.0

func (s *MemoryKeyStore) Lookup(_ context.Context, rawKey string) (*KeyRecord, bool, error)

Lookup matches the raw key against all active records using constant-time comparison, without early exit, so match timing does not depend on which record (if any) matched.

func (*MemoryKeyStore) Remove added in v0.5.0

func (s *MemoryKeyStore) Remove(_ context.Context, id string) error

Remove deletes a record by ID.

type MemoryQuotaStore added in v0.5.0

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

MemoryQuotaStore is an in-memory QuotaStore with a rolling one-minute request window and a rolling 24h token/cost window. It is safe for concurrent use.

func NewMemoryQuotaStore added in v0.5.0

func NewMemoryQuotaStore() *MemoryQuotaStore

NewMemoryQuotaStore returns an empty in-memory quota store.

func (*MemoryQuotaStore) AddUsage added in v0.5.0

func (s *MemoryQuotaStore) AddUsage(_ context.Context, subject string, tokens int64, costUSD float64) error

AddUsage records token and cost consumption for the subject.

func (*MemoryQuotaStore) Allow added in v0.5.0

func (s *MemoryQuotaStore) Allow(_ context.Context, subject string, q Quota) (bool, error)

Allow admits one request against the subject's rolling windows and reports whether all configured limits are satisfied. When a limit is exceeded, the request is not counted so a rejected caller does not deepen its own deficit.

func (*MemoryQuotaStore) Usage added in v0.5.0

func (s *MemoryQuotaStore) Usage(_ context.Context, subject string) (Usage, error)

Usage returns the current usage snapshot for the subject.

type Quota added in v0.5.0

type Quota struct {
	// RequestsPerMinute caps requests within a rolling one-minute window.
	// Enforced by APIKeyMiddleware.
	RequestsPerMinute int `json:"requests_per_minute,omitempty"`
	// TokensPerDay caps model tokens within a rolling 24h window. Enforced only
	// once AddUsage is wired by the integrator (see the type doc).
	TokensPerDay int64 `json:"tokens_per_day,omitempty"`
	// MaxCostUSD caps cumulative spend (USD) within a rolling 24h window.
	// Enforced only once AddUsage is wired by the integrator (see the type doc).
	MaxCostUSD float64 `json:"max_cost_usd,omitempty"`
}

Quota defines rate and budget limits for a subject (an API key or a tenant). A zero value in any field means that dimension is unlimited.

Enforcement note: RequestsPerMinute is enforced out of the box by APIKeyMiddleware at request admission. TokensPerDay and MaxCostUSD are checked by Allow against recorded usage, but that usage is only populated when the integrator calls QuotaStore.AddUsage after a model call — the HTTP admission point has no visibility into token/cost, so these budgets do nothing until AddUsage is wired. For in-process token/cost budgeting that intercepts model usage directly, see engine/hooks/cost.go (CostTracker), which operates at the engine layer where usage actually exists.

func (Quota) IsZero added in v0.5.0

func (q Quota) IsZero() bool

IsZero reports whether the quota imposes no limits.

type QuotaStore added in v0.5.0

type QuotaStore interface {
	// Allow records one request against subject and reports whether it is within
	// the given quota (rate + budget). A subject over any limit returns false.
	Allow(ctx context.Context, subject string, q Quota) (bool, error)
	// AddUsage records token and cost consumption for a subject.
	AddUsage(ctx context.Context, subject string, tokens int64, costUSD float64) error
	// Usage returns the current usage snapshot for a subject.
	Usage(ctx context.Context, subject string) (Usage, error)
}

QuotaStore enforces and records per-subject usage. It is the extension point the control plane wires to attach token/cost budgets to requests:

  • Allow is called at request admission time (enforces RequestsPerMinute, and TokensPerDay/MaxCostUSD against whatever usage has been recorded).
  • AddUsage records token/cost spend and MUST be called by the integrator after model calls for the token/cost budgets to have any effect; it is not auto-wired because model usage is not available at HTTP admission. See the Quota doc and engine/hooks/cost.go for the in-engine alternative.

type Role

type Role string

Role defines an RBAC role.

const (
	RoleAdmin  Role = "admin"
	RoleUser   Role = "user"
	RoleViewer Role = "viewer"
)

type Service

type Service struct{}

Service manages authentication and authorization.

func NewService

func NewService() *Service

func (*Service) CheckPermission

func (s *Service) CheckPermission(claims *UserClaims, required Role) bool

CheckPermission verifies a user has the required role.

type Usage added in v0.5.0

type Usage struct {
	Requests int     `json:"requests"`
	Tokens   int64   `json:"tokens"`
	CostUSD  float64 `json:"cost_usd"`
}

Usage is a point-in-time snapshot of consumption for a subject.

type UserClaims added in v0.3.0

type UserClaims struct {
	UserID   string   `json:"user_id,omitempty"`
	Roles    []string `json:"roles,omitempty"`
	Exp      int64    `json:"exp,omitempty"`
	Issuer   string   `json:"iss,omitempty"`
	Audience Audience `json:"aud,omitempty"`
	Subject  string   `json:"sub,omitempty"`
	TenantID string   `json:"tenant_id,omitempty"`
}

UserClaims represents the decoded user information from a JWT.

func UserFromContext added in v0.3.0

func UserFromContext(ctx context.Context) (*UserClaims, bool)

UserFromContext extracts UserClaims from the request context.

Jump to

Keyboard shortcuts

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