Skip to content

token-validator

Stable

Purpose

The token-validator plugin validates bearer tokens against one or more configured OIDC/OAuth2 issuers before forwarding a request upstream. Instead of embedding JWT verification in every service, the gateway validates once and injects identity headers (X-Actor-Principal, X-Actor-Roles, X-Tenant-ID) so downstream services can trust the gateway-propagated identity without parsing tokens themselves.

Pipeline position:

flowchart LR
TV["**token-validator**"]:::active --> TI["tenant-injector"]
TI --> LC["license-check"]
LC --> PS["prompt-sanitize"]
PS --> OA["otel-audit"]
OA --> UP["upstream"]
classDef active fill:#4ade80,stroke:#16a34a,color:#14532d

token-validator is pipeline position 1 — the first plugin every request passes through.

Config

gateway.yaml (token-validator block)
token-validator:
issuers:
- url: https://auth.example.com/realms/my-realm
audience: my-service
jwks_cache_ttl: 300s
claim_mappings:
subject: sub
roles: realm_access.roles
tenant: tenant_id
- url: https://accounts.google.com
audience: my-google-client-id
jwks_cache_ttl: 600s
claim_mappings:
subject: sub
roles: groups
algorithms:
- RS256
- ES256
clock_skew_seconds: 10
required_claims:
- sub
propagate_claims:
mode: all
on_failure:
missing_token: 401
invalid_signature: 401
expired: 401
unknown_issuer: 401
audience_mismatch: 401
jwks_unavailable: 503
max_token_bytes: 16384
FieldTypeDefaultRequiredDescription
issuerslistyes (v2 mode)One entry per trusted OIDC/OAuth2 issuer.
issuers[].urlstring (URL)yesIssuer URL; must match the iss claim in incoming tokens exactly.
issuers[].audiencestringyesRequired aud claim value for this issuer.
issuers[].jwks_cache_ttlduration300snoHow long to cache the issuer’s JWKS response.
issuers[].claim_mappings.subjectstring"sub"noClaim path to extract the actor identity into X-Actor-Principal.
issuers[].claim_mappings.rolesstringnoClaim path (dot-notation) for roles → X-Actor-Roles.
issuers[].claim_mappings.tenantstringnoClaim path for tenant ID → X-Tenant-ID. Omit to delegate tenant resolution to the tenant-injector plugin.
algorithmslist[RS256, ES256]noAllowed signing algorithms. none is always rejected.
clock_skew_secondsinteger0noTolerance for clock drift on exp/nbf/iat. Max 600.
required_claimslist[]noClaims that must be non-empty; request fails with 401 if absent.
propagate_claims.modestring"all"noall forwards all validated claims to upstreams; allowlist restricts to propagate_claims.claims.
propagate_claims.claimslistno (required when mode=allowlist)Explicit claim names to propagate.
on_failure.<class>integersee belownoOverride the HTTP status returned for each failure class.
max_token_bytesinteger16384noReject tokens larger than this (bytes). Guards against oversized-token DoS.

Default on_failure status codes (v2 mode): missing_token → 401, invalid_signature → 401, expired → 401, not_yet_valid → 401, unknown_issuer → 401, audience_mismatch → 401, required_claim_missing → 401, disallowed_algorithm → 401, oversized_token → 400, jwks_unavailable → 503.

Request/Response

Reads from request

SourceFieldHow used
Authorization headerBearer <token>JWT extracted and validated.
Token iss claimMatched against issuers[].url to select the issuer config.
Token aud claimVerified against issuers[].audience.
Token exp, nbf, iatVerified with clock_skew_seconds tolerance.

Writes to request (before forwarding upstream)

HeaderContentWhen injected
X-Actor-PrincipalValue of claim_mappings.subject claim (e.g. sub)Always, when claim present and claim_mappings.subject configured.
X-Actor-RolesJSON array of roles from claim_mappings.roles pathWhen claim_mappings.roles configured and claim present.
X-Tenant-IDValue of claim_mappings.tenant claimWhen claim_mappings.tenant configured and claim present.

Existing X-Actor-Principal, X-Actor-Roles, and X-Tenant-ID headers from the original request are stripped before forwarding — clients cannot inject their own identity.

Writes to response

This plugin does not modify responses. It may return an early rejection response; see Status codes below.

Status codes the plugin can return early

StatusMedia typeWhen
401application/vnd.yaagents.error+jsonNo Authorization header; token expired; signature invalid; issuer not in issuers list; audience mismatch; required claim absent; disallowed algorithm. Includes WWW-Authenticate: Bearer realm="yaagents-gateway" (RFC 7235 §3.1).
400application/vnd.yaagents.error+jsonToken exceeds max_token_bytes.
503application/vnd.yaagents.error+jsonJWKS endpoint unreachable or returned a non-2xx response.

Security & privacy

What this plugin trusts

  • The JWKS served by each configured issuers[].url endpoint (fetched over HTTPS; cached per jwks_cache_ttl).
  • The iss claim in the unverified JWT header — used only to select the issuer config, never to grant access on its own.
  • The gateway’s own system clock (modulo clock_skew_seconds) for exp/nbf/iat checks.

What this plugin protects

  • Token forgery: rejects any JWT whose signature cannot be verified against the issuer’s JWKS.
  • Issuer confusion: tokens from an unconfigured issuer are rejected (unknown-issuer path → 401). A token signed by a legitimate-but-unconfigured IDP is not accepted.
  • Header smuggling: strips X-Actor-Principal, X-Actor-Roles, and X-Tenant-ID from inbound requests before forwarding — clients cannot pre-inject identity headers.
  • Oversized tokens: max_token_bytes guard prevents parse-amplification attacks on the JWT payload.
  • Signing algorithm confusion: algorithms allowlist is enforced before signature verification; alg: none is unconditionally rejected regardless of the allowlist.

PII boundary

JWT claims (including sub, role lists, and any tenant ID) are extracted and injected as request headers — they are not logged. The raw token itself is never written to any log line or span attribute. claim_mappings controls exactly which claims reach X-Actor-Principal, X-Actor-Roles, and X-Tenant-ID; all other claims stay inside the validated token object and are discarded after the request context is populated.

Secrets handling

The plugin receives no secrets at runtime. JWKS keys are fetched from the public JWKS endpoint of each configured issuer (issuers[].url/.well-known/jwks.json) over HTTPS. No API keys, client credentials, or private keys are loaded from config, environment variables, or Parameter Store — the issuer URL is the only configuration sensitive to misconfiguration, not to leakage.

For test_mode: true with jwt_secret (HS256 local dev), the secret is read from the jwt_secret config key; never commit YAML with a real secret inline.

Observability

Spans / events emitted

Span nameAttributesWhen emitted
token.validateissuer, audience, algorithm, outcome (ok | fail)Every request.
jwks.fetchissuer_url, status (hit | miss | error)On JWKS cache lookup; miss or error only when a network fetch occurs.

Bench baseline (BENCH-1; commit 5c657b8; 2026-06-07): p99 overhead +4.9 ms vs no-plugin baseline at 100 RPS, 2-issuer config with per-issuer JWKS cache TTL 300 s. Primarily dominated by JWKS cache lookup; warm-cache requests add sub-millisecond overhead. See Audit and Observability for the full gateway observability baseline and bench result archive.

Log lines

{"level":"INFO","msg":"token.validate","issuer":"https://auth.example.com/realms/my-realm","algorithm":"RS256","outcome":"ok","request_id":"req-abc123"}
{"level":"WARN","msg":"jwks.fetch","issuer_url":"https://auth.example.com/...","status":"error","error":"connection refused","request_id":"req-abc124"}

WARN-level log on JWKS fetch failure; ERROR-level log on Init failure (startup abort). No token content, raw claims, or sub values appear in log lines.

Metrics

MetricTypeLabelsDescription
yaagents_plugin_token_validate_totalcounterissuer, outcomeCumulative validation count by issuer and outcome.
yaagents_plugin_jwks_fetch_totalcounterissuer, statusJWKS fetches by issuer and status (hit, miss, error).
yaagents_plugin_jwks_cache_age_secondshistogramissuerAge of the cached JWKS at validation time (bucket insight for tuning jwks_cache_ttl).

Correlation-id propagation

Reads X-Correlation-ID from the inbound request and attaches it as the correlation_id attribute on the token.validate span. Forwards X-Correlation-ID to upstream unchanged (per PRD §9 gateway responsibilities). Does not generate a new correlation ID — that is the gateway bootstrap layer’s responsibility.

Failure modes

FailureConfigurable behaviorWhat the client sees
No Authorization headeron_failure.missing_token (default 401)401 application/vnd.yaagents.error+json + WWW-Authenticate header.
Token signature invalidon_failure.invalid_signature (default 401)401 application/vnd.yaagents.error+json + WWW-Authenticate.
Token expiredon_failure.expired (default 401)401 application/vnd.yaagents.error+json.
iss not in issuers liston_failure.unknown_issuer (default 401)401 application/vnd.yaagents.error+json.
aud mismatchon_failure.audience_mismatch (default 401)401 application/vnd.yaagents.error+json.
Required claim absenton_failure.required_claim_missing (default 401)401 application/vnd.yaagents.error+json.
Disallowed algorithm (incl. none)on_failure.disallowed_algorithm (default 401)401 application/vnd.yaagents.error+json.
Token exceeds max_token_bytesFixed 400 (not overridable)400 application/vnd.yaagents.error+json.
JWKS endpoint unreachableon_failure.jwks_unavailable (default 503)503 application/vnd.yaagents.error+json. Requests fail closed — no pass-through mode.