Verifies opaque OAuth 2.0 access tokens by calling the IdP's RFC 7662
introspection endpoint. The result is cached per-token until exp (or
maxCacheTtl) so the IdP sees one round-trip per token.
Source: pkg/identity/introspection — registered as oauth2-introspection.
- IdP issues opaque tokens (random strings, no
.separators). - IdP exposes
/oauth2/introspect(Keycloak, Auth0, Hydra, Okta). - You can tolerate a bounded IdP round-trip per fresh token.
Don't use for self-contained JWTs — jwt is dramatically cheaper.
identifiers:
- name: opaque-bearer
type: oauth2-introspection
config:
url: https://idp.example.com/oauth2/introspect # REQUIRED
clientId: my-api # for Basic auth to /introspect
clientSecret: ${INTROSPECT_SECRET} # see warning below
headerName: Authorization # default
# cacheSize bounds in-process LRU; cluster-shared cache uses the
# `valkey` backend selected on AuthConfig.cache.backend.
cacheSize: 10000
maxCacheTtl: 5m # cap if claims.exp is far in the future
negativeTtl: 10s # how long to remember "active: false"
errorTtl: 5s # how long to remember "IdP failed for this token"!!! warning "clientSecret is read literally — no ${VAR} substitution"
lwauth does not expand ${INTROSPECT_SECRET}-style placeholders
anywhere in AuthConfig. Two ways to actually inject it:
clientSecret: "vault://kv/lwauth/introspection#secret" (resolved
at compile time — any string field in a module's config: is
checked recursively, internal/config/loader.go's
resolveMapSecrets), or template the AuthConfig YAML itself at
the deployment-pipeline layer (Helm, Kustomize, CI) so the real
secret is already inlined before lwauth ever parses it.
The cache key is sha256(token); entries TTL = min(claims.exp - now, maxCacheTtl).
Three cache lines run in parallel, all keyed by the same digest:
| Line | What it caches | TTL |
|---|---|---|
| positive | active: true claims |
min(claims.exp - now, maxCacheTtl) |
| negative | a sentinel for active: false |
negativeTtl |
| error | a sentinel for ErrUpstream outcomes (network failure, 5xx, circuit-open) |
errorTtl |
The error cache (added in v1.1, K-AUTHN-2) closes a per-request DoS
amplification window: without it, every retry while the IdP is wounded
re-dials the IdP. The upstream Guard circuit-breaker
trips per (tenant, upstream) pair; this cache adds per-credential
coalescing on top so a token-spray during an IdP blip doesn't fan
out into thousands of upstream calls before the breaker opens.
Set errorTtl: 0 to disable the error cache entirely — useful if you
front lwauth with another rate limiter that already absorbs IdP failure
amplification, or if you require every request to test reachability.
The negative cache (negativeTtl) is unaffected by this setting.
Concurrent first-misses for the same token still collapse to one IdP
call via singleflight regardless of cache configuration.
Revocation note. Cached results survive an IdP-side revocation until TTL. Use short token TTLs or opt into the M14 revocation surface if you need stronger guarantees.
# values.yaml
config:
inline: |
identifiers:
- name: opaque-bearer
type: oauth2-introspection
config:
url: https://idp.example.com/oauth2/introspect
clientId: my-api
clientSecret: "vault://kv/lwauth/introspection#secret"
authorizers:
- { name: any-auth, type: rbac, config: { allow: ["*"] } }secretRef:-style resolution (the vault:// scheme above) already
shipped in pkg/secrets — see the warning above; there's no
Kubernetes-Secret-via-env-var equivalent (setting clientSecret: ${INTROSPECT_SECRET} and expecting a pod env var named
INTROSPECT_SECRET to fill it in does not work — lwauth performs no
${VAR} expansion at all).
First request with a fresh token:
client → lwauth → POST /oauth2/introspect (token=...)
← {"active":true,"sub":"alice","exp":1731000000,...}
→ cache.Set(sha256(token), 5m TTL)
→ continue pipeline
Subsequent requests with the same token:
client → lwauth → cache.Get(sha256(token)) → hit → continue
- Stack with
jwtunderfirstMatchso signed tokens skip the network call entirely. - Use
valkeycache backend to share introspection results across replicas — saves IdP load when the same token hits different Pods.
- RFC 7662 (Token Introspection).
- DESIGN.md §4 — token introspection.
- Source: pkg/identity/introspection/introspection.go.