auth

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package auth implements the JWT validation and Astarte authorization-claim layer shared by every Astrate REST surface (docs/DESIGN.md §4.2). It reproduces upstream Astarte's token semantics exactly — asymmetric keys only (RSA/ECDSA, `none` and HMAC hard-rejected), per-realm multi-key rotation, and `"<verb-regex>:<opts>:<path-regex>"` authorization strings matched with implicit anchoring against the request method and the path relative to the realm base — so tokens minted by astartectl and existing operator tooling work unmodified.

The package is pure (no database): key material is injected through the KeySource interface, which *store.Store already satisfies.

Index

Constants

View Source
const (
	VerbJoin  = "JOIN"
	VerbWatch = "WATCH"
)

Channels authorization verbs. Upstream recognises exactly these two and silently discards an a_ch entry carrying anything else.

View Source
const DefaultCacheSize = 1024

DefaultCacheSize is the verified-token LRU capacity (docs/DESIGN.md §4.2).

Variables

View Source
var (
	// ErrNoRealmKeys reports that the realm has no JWT public keys
	// configured, so no token can possibly verify.
	ErrNoRealmKeys = errors.New("auth: realm has no JWT public keys")
	// ErrNoKeyMatched reports that the token signature verified against
	// none of the realm's keys (wrong key, tampered token, or disallowed
	// algorithm).
	ErrNoKeyMatched = errors.New("auth: token matches none of the realm keys")
	// ErrUnsupportedKey reports PEM key material that is neither an RSA nor
	// an ECDSA public key.
	ErrUnsupportedKey = errors.New("auth: unsupported public key type")
)

Sentinel errors returned by token verification. All of them map to 401 at the HTTP layer; they are distinct so logs and tests can tell causes apart.

Functions

func ParsePublicKeysPEM

func ParsePublicKeysPEM(pems []string) ([]crypto.PublicKey, error)

ParsePublicKeysPEM parses a realm's JWT public key set. Each entry may carry one or more PEM blocks; supported block types are PKIX "PUBLIC KEY" and PKCS#1 "RSA PUBLIC KEY", and the decoded keys must be RSA or ECDSA (matching the signing-method allowlist). An entry with no usable key is an error: silently dropping keys would turn a key-set typo into a hard 401 for every token holder.

func RelativePath

func RelativePath(urlPath, base string) (string, bool)

RelativePath computes the authorization path with upstream parity (Astarte's GuardianAuthorizePath plug): split the URL path into segments, drop everything up to and including the first segment equal to base, and join the rest with "/". ok is false when base does not appear in the path.

Example: RelativePath("/pairing/v1/test/agent/devices", "test") returns ("agent/devices", true).

Types

type Cache

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

Cache memoizes verified tokens so the signature check, claim validation, and regex compilation run once per (token, key set) instead of once per request. Entries are keyed by SHA-256 of the token *and* of the key set, so rotating a realm's keys naturally invalidates its cached tokens.

func NewCache

func NewCache(size int) *Cache

NewCache builds a Cache with the given capacity (values < 1 fall back to DefaultCacheSize).

func (*Cache) Verify

func (c *Cache) Verify(tokenString string, keysPEM []string) (*Token, error)

Verify returns the verified Token for tokenString against the realm's PEM key set, from cache when possible. Cache hits still honour `exp`: a token that expired while cached is evicted and rejected. Failed verifications are not cached.

type Claim

type Claim string

Claim names an Astarte authorization claim: the JWT key under which a list of authorization strings is carried (docs/DESIGN.md §4.2).

const (
	// ClaimAppEngine authorizes the AppEngine API (a_aea).
	ClaimAppEngine Claim = "a_aea"
	// ClaimChannels authorizes Astarte Channels; Astrate honours it on the
	// live stream socket (a_ch).
	ClaimChannels Claim = "a_ch"
	// ClaimFlow authorizes the Flow API (a_f). Upstream flow guards every
	// route with this claim via GuardianAuthorizePath against {realm}/{path}.
	ClaimFlow Claim = "a_f"
	// ClaimHousekeeping authorizes the Housekeeping API (a_ha).
	ClaimHousekeeping Claim = "a_ha"
	// ClaimPairing authorizes the Pairing agent API (a_pa).
	ClaimPairing Claim = "a_pa"
	// ClaimRealmManagement authorizes the Realm Management API (a_rma).
	ClaimRealmManagement Claim = "a_rma"
)

The Astarte claim set. Each claim authorizes one API surface; the values are the exact JWT keys upstream tooling (astartectl, astarte-go) emits.

type KeySource

type KeySource interface {
	GetRealmByName(ctx context.Context, name string) (*store.Realm, error)
}

KeySource supplies per-realm key material. *store.Store satisfies it; the middleware only reads JWTPublicKeysPEM from the returned realm.

type Middleware

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

Middleware authenticates and authorizes REST requests with realm JWTs (docs/DESIGN.md §4.2). Status mapping is upstream parity: missing or unverifiable token → 401, verified token whose claims do not authorize the request → 403, both with the canonical envelopes.

func NewMiddleware

func NewMiddleware(keys KeySource) *Middleware

NewMiddleware builds a Middleware over the given key source with a DefaultCacheSize token cache.

func (*Middleware) RequireRealm

func (m *Middleware) RequireRealm(claim Claim) func(http.Handler) http.Handler

RequireRealm guards a realm-scoped route (path pattern must carry a {realm} segment): it resolves the realm's JWT public keys, verifies the bearer token, and matches the claim's authorization strings against the method and the path relative to the realm base.

func (*Middleware) RequireRealmAny added in v0.2.0

func (m *Middleware) RequireRealmAny(claims ...Claim) func(http.Handler) http.Handler

RequireRealmAny guards a realm-scoped route accepting ANY of the given claims (OR-ed): the first claim whose authorization strings match the method/path grants access. Used by surfaces that honour both their upstream claim and an Astrate compatibility claim — e.g. Flow accepts a_f (upstream) and a_rma (Astrate's original operator claim).

func (*Middleware) RequireStatic

func (m *Middleware) RequireStatic(claim Claim, keysPEM []string) func(http.Handler) http.Handler

RequireStatic guards an instance-level route (Housekeeping) with a fixed key set instead of per-realm keys. The authorization path is the request path relative to the service base (the segment after "v1").

type Token

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

Token is a verified JWT: its expiry and its compiled authorization grants. Tokens are immutable and safe for concurrent use, which is what allows the LRU cache to hand the same *Token to many requests.

func TokenFromContext

func TokenFromContext(ctx context.Context) (*Token, bool)

TokenFromContext returns the verified token stored by the middleware, if the request passed through Require*.

func Verify

func Verify(tokenString string, keys []crypto.PublicKey, now func() time.Time) (*Token, error)

Verify parses tokenString, verifies its signature against the key set, and validates its registered claims (`exp` and `nbf` are honoured when present; `iat` is not required — upstream parity, docs/DESIGN.md §4.2). The token verifies if *any* key in the set matches, which is what makes zero-downtime key rotation work. now supplies the validation clock.

func (*Token) Authorizes

func (t *Token) Authorizes(claim Claim, verb, authPath string) bool

Authorizes reports whether the token grants `claim` for the given verb (HTTP method, or JOIN/WATCH on the stream socket) and authorization path (the request path relative to the realm base, e.g. "agent/devices"). Multiple authorization strings within a claim are OR-ed; a token without the claim authorizes nothing on that surface.

func (*Token) AuthorizesChannel added in v0.2.0

func (t *Token) AuthorizesChannel(verb, authPath string) bool

AuthorizesChannel reports whether the a_ch claim grants verb (VerbJoin or VerbWatch) on authPath — the room name for a join, the trigger's target for a watch.

It deliberately does not reuse Authorizes, because upstream reads a_ch by a different rule than the REST claims and the difference is observable. The REST plug compiles the verb field into a regex and matches it against the HTTP method, so "GET|POST" works and ".*::.*" grants everything. The Channels socket instead *partitions* the a_ch list by an exact match on the verb field, keeping only entries whose first field is literally "JOIN" or "WATCH" and discarding the rest, then matches the path regex within the chosen bucket.

So a blanket ".*::.*" — which authorizes every REST surface — authorizes nothing here: ".*" is not the string "JOIN". This is measured, not inferred; see test/conformance/upstream/channels.json, where upstream refuses a join under ".*::.*" and accepts the same join under "JOIN::.*".

func (*Token) ExpiresAt

func (t *Token) ExpiresAt() (time.Time, bool)

ExpiresAt returns the token expiry and whether one is set (`exp` is optional, upstream parity).

Jump to

Keyboard shortcuts

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