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 ¶
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.
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.
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 ¶
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.
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 ¶
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 ¶
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 WithDiscoveryDone ¶
func WithDiscoveryDone(ch chan struct{}) Option
WithDiscoveryDone arranges for ch to be closed when the background discovery loop exits.
func WithHTTPClient ¶
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 ¶
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 ¶
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.