Skip to content

Latest commit

 

History

History
128 lines (101 loc) · 5.1 KB

File metadata and controls

128 lines (101 loc) · 5.1 KB

oauth2-introspection — Opaque bearer tokens

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.

When to use

  • 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.

Configuration

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.

Helm wiring

# 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).

Worked example

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

Composition

  • Stack with jwt under firstMatch so signed tokens skip the network call entirely.
  • Use valkey cache backend to share introspection results across replicas — saves IdP load when the same token hits different Pods.

References