delegated

package
v1.8.9 Latest Latest
Warning

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

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

Documentation

Overview

Package delegated verifies externally issued OAuth 2.0 access tokens (RFC 9068 `at+jwt`) minted by a configured OIDC issuer, so an operator's control plane can call the e2a API on behalf of its own signed-in humans without holding e2a credentials. It is disabled by default and fully generic: every deployment-specific value (issuer, audience, authorized party, scope, claim policy) arrives via configuration — nothing in this package names any particular operator.

The package owns exactly one token kind: a compact JWT whose protected JOSE header carries `typ":"at+jwt"`. Classification (Classify) inspects only the protected header; ownership is decided before any signature or network work, and an owned token never falls through to e2a's other credential paths — a malformed or invalid delegated token is a 401, not an API-key probe.

Every size limit below is part of the delegated-token contract and is enforced byte-for-byte (and, where stated, code-point-for-code-point). The limits apply to delegated verification only; existing agent-JWT and API-key contracts are unchanged.

Index

Constants

View Source
const (
	// MaxAuthorizationBytes caps the raw Authorization header, including
	// the "Bearer " prefix, in ASCII bytes.
	MaxAuthorizationBytes = 16384
	// MaxCompactJWTBytes caps the compact token (three non-empty
	// base64url segments) in ASCII bytes.
	MaxCompactJWTBytes = 16384
	// MaxProtectedHeaderBytes caps the base64url-DECODED protected header.
	MaxProtectedHeaderBytes = 1024
	// MaxPayloadBytes caps the base64url-decoded payload.
	MaxPayloadBytes = 8192
	// MaxSignatureBytes caps the base64url-decoded signature.
	MaxSignatureBytes = 1024
	// MaxTypBytes caps the protected `typ` value in ASCII bytes.
	MaxTypBytes = 32
	// MaxAlgBytes caps the protected `alg` value in ASCII bytes.
	MaxAlgBytes = 16
	// MaxKidBytes caps a protected or JWKS `kid` in ASCII bytes.
	MaxKidBytes = 128
	// MaxTopLevelClaims caps the number of top-level members in the JWT
	// claims object.
	MaxTopLevelClaims = 32
	// MaxClaimNameBytes caps a claim name in ASCII bytes. Claim names are
	// ASCII with no C0 control characters.
	MaxClaimNameBytes = 128

	// Dual code-point/byte limits for string claims. Both bounds apply
	// independently: a value fails at either limit+1.
	MaxIssuerCodePoints   = 512
	MaxIssuerBytes        = 2048
	MaxAudienceCodePoints = 256
	MaxAudienceBytes      = 1024
	MaxAzpCodePoints      = 128
	MaxAzpBytes           = 512
	// MaxScopeBytes caps the scope claim in ASCII bytes.
	MaxScopeBytes = 128
	// Subject-class claims: sub, jti, and every configured required
	// context claim share one limit, as does a provisioning external_ref.
	MaxSubjectCodePoints = 128
	MaxSubjectBytes      = 512
)

Exact credential size limits. Each rejects at limit+1.

View Source
const (
	// FetchTimeout bounds one discovery or JWKS HTTP fetch.
	FetchTimeout = 10 * time.Second
	// MaxJWKSBytes caps the decoded JWKS response body.
	MaxJWKSBytes = 65536
	// MaxJWKSKeys caps accepted JWKs per issuer.
	MaxJWKSKeys = 32
	// KeysFreshFor is how long a successfully fetched keyset serves any
	// cached kid without a refresh.
	KeysFreshFor = 600 * time.Second
	// KeysStaleGrace extends KeysFreshFor for kids already present in the
	// last good set. Past KeysFreshFor+KeysStaleGrace even a known key is
	// verifier-unavailable until a refresh succeeds.
	KeysStaleGrace = 300 * time.Second
	// RefreshCooldown is the negative-result cooldown: after a failed
	// refresh, further refresh attempts fail without fetching.
	RefreshCooldown = 10 * time.Second
	// Refresh token bucket: RefreshBurst immediate refresh, refilling at
	// RefreshPerWindow per RefreshWindow.
	RefreshBurst     = 1
	RefreshPerWindow = 6
	RefreshWindow    = 60 * time.Second
)

JWKS/discovery policy constants.

View Source
const TokenType = "at+jwt"

TokenType is the protected-header `typ` this package owns, compared as the exact string (RFC 9068 registers the "application/at+jwt" media type; the issuer contract here mints exactly this short form).

Variables

View Source
var ErrInvalidToken = errors.New("delegated: invalid token")

ErrInvalidToken is the terminal "this token is bad" class: signature, type, algorithm, issuer, audience, azp, scope, time, claim, or size failures, and an unknown kid after a successful refresh. Callers must map it to 401 with no check-specific detail.

View Source
var ErrUnavailable = errors.New("delegated: verifier unavailable")

ErrUnavailable is the dynamic availability class: discovery not yet complete, JWKS transport/parse failure with no usable cached key, or a refresh denied by cooldown/rate limiting. Callers must map it to 503 (never a WWW-Authenticate challenge) — it says nothing about the token.

Functions

func Classify

func Classify(rawAuthorizationLen int, bearer string) bool

Classify reports whether this bearer credential is delegated-owned: a compact JWT, within the raw header/compact/segment limits, whose protected header carries exactly typ "at+jwt".

The decision is parse-only — no signature, network, or configuration is consulted, so ownership is identical whether the verifier is enabled, disabled, or unavailable. A positively classified token must never reach any other credential path. Classification reads only `typ` and is tolerant of every other header member; the verification-time pins (alg/kid caps, strict typing) run later in Verify and reject a bad token as a delegated 401, never by handing it back to another path.

rawAuthorizationLen is the length of the full Authorization header value, including the "Bearer " prefix, so the raw-header cap covers what actually arrived on the wire.

Types

type Claims

type Claims struct {
	Issuer  string
	Subject string
}

Claims is what verification exposes to authentication: the exact configured issuer the token verified against and the token's opaque subject. Identity mapping looks these up as a pair — never the subject alone — and nothing else from the token (profile claims, context claims) reaches identity resolution.

type Config

type Config struct {
	// IssuerURL is used for OIDC discovery and exact byte-for-byte `iss`
	// comparison. No alias or trailing-slash normalization.
	IssuerURL string
	// Audience is the exact single-string `aud`. Arrays are rejected even
	// when they contain this value.
	Audience string
	// AuthorizedParty is the exact required `azp`.
	AuthorizedParty string
	// RequiredScope is the exact singleton scope string — the claim must
	// equal it, not merely contain it.
	RequiredScope string
	// AllowedAlgorithms is the closed signature-algorithm allowlist
	// (subset of RS256/ES256 — the only algorithms this verifier
	// implements).
	AllowedAlgorithms []string
	// MaxTokenLifetime bounds `exp - iat`.
	MaxTokenLifetime time.Duration
	// ClockSkew admits an `iat` up to this far in the future and an `exp`
	// up to this far in the past. It never relaxes MaxTokenLifetime.
	ClockSkew time.Duration
	// RequiredClaims are context claims that must be present as bounded
	// nonempty strings.
	RequiredClaims []RequiredClaim
	// ForbiddenClaims must be absent entirely — present-as-null still
	// rejects.
	ForbiddenClaims []string
}

Config carries the deployment's verification policy. All values are deployment data — see the config package for validation; NewVerifier re-checks only what it cannot function without.

type Option

type Option func(*Verifier)

Option customizes a Verifier; test seams only.

func WithClock

func WithClock(now func() time.Time) Option

WithClock injects the time source.

func WithDiscoveryDone

func WithDiscoveryDone(ch chan struct{}) Option

WithDiscoveryDone arranges for ch to be closed when the background discovery loop exits.

func WithHTTPClient

func WithHTTPClient(c *http.Client) Option

WithHTTPClient injects the HTTP client used for discovery and JWKS.

type RefreshMetrics

type RefreshMetrics interface {
	DelegatedJWKSRefresh(outcome string)
}

RefreshMetrics is the narrow observability seam for key-refresh outcomes. telemetry.Metrics satisfies it; nil disables emission. Outcome values are a closed set: success, key_absent, transport_error, parse_error, rate_limited — never key material, kids, issuer response text, or token data.

type RequiredClaim

type RequiredClaim struct {
	Name          string
	AllowedValues []string
}

RequiredClaim names one required nonempty string claim and, optionally, its closed set of allowed values.

type Verifier

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

Verifier is the one long-lived delegated-token verifier for the single configured issuer. Construction never performs network I/O: discovery runs on a background retry loop, so issuer unavailability degrades delegated authentication (503) without affecting startup or any other credential path.

func NewVerifier

func NewVerifier(ctx context.Context, cfg Config, metrics RefreshMetrics, opts ...Option) (*Verifier, error)

NewVerifier validates the static policy and starts background issuer discovery bounded by ctx. A static-policy error here is a misconfiguration the caller should treat as startup-fatal; network state never surfaces here.

func (*Verifier) Verify

func (v *Verifier) Verify(ctx context.Context, bearer string) (*Claims, error)

Verify authenticates one delegated-owned compact token end to end: exact size limits, protected-header pins, cached-key signature verification, and the full claim policy. On success it returns the verified (issuer, subject) pair and nothing else. Errors are ErrInvalidToken (401 class) or ErrUnavailable (503 class) — callers must not surface which check failed.

Jump to

Keyboard shortcuts

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