---
title: "JWT authentication"
url: "/docs/security/jwt/"
kind: "feature"
summary: "Validate JSON Web Tokens against local or remote JWKS, choose which routes require which provider, and forward claims to backends."
---

> Arion site index: [/llms.txt](/llms.txt)

# JWT authentication

Validate JSON Web Tokens against local or remote JWKS, choose which routes require which provider, and forward claims to backends.

The JWT authentication filter verifies a signed token on every request before it is routed: signature against a JWKS, plus issuer and audience checks. Use it when clients authenticate with bearer tokens from an identity provider and the gateway — not each backend — should reject invalid callers.

## Enable it

Add `envoy.filters.http.jwt_authn` to the HTTP filter chain (before the router). A configuration has two parts: `providers` describe how to validate a token, `rules` decide which provider a request must satisfy.

```yaml,name=arion.yaml,arion=fragment
http_filters:
  - name: envoy.filters.http.jwt_authn
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
      providers:
        idp:
          issuer: https://idp.example.com
          audiences: [api.example.com]
          from_headers:
            - name: Authorization
              value_prefix: "Bearer "
          local_jwks:
            filename: jwks.json
      rules:
        - match: { prefix: "/" }
          requires: { provider_name: idp }
  - name: envoy.filters.http.router
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
```

Rules are evaluated in order and the first whose `match` fits the request decides the provider. A request that no rule maps to a provider is rejected — with this filter in the chain, every route needs a rule.

> [!IMPORTANT]
> Always configure at least one token source (`from_headers`, `from_params` or `from_cookies`). Arion has no implicit default; a provider without one rejects every request with 401.

## Remote JWKS

Fetch keys from the identity provider instead of shipping them in the config. The JWKS host needs its own cluster; keys are fetched in the background and refreshed every `cache_duration` (default 60s), with an optional `retry_policy`.

```yaml,arion=fragment
providers:
  idp:
    issuer: https://idp.example.com
    audiences: [api.example.com]
    from_headers:
      - name: Authorization
        value_prefix: "Bearer "
    remote_jwks:
      http_uri:
        uri: https://idp.example.com/.well-known/jwks.json
        cluster: idp
        timeout: 5s
      cache_duration: 300s
```

## Token sources

A provider looks for the token in its configured `from_headers` (each with a `value_prefix` to strip, such as `"Bearer "`), then `from_params` query parameters, then `from_cookies`. After successful validation the token is removed from the forwarded request — header deleted, query parameter stripped — unless the provider sets `forward: true`.

## Forward claims

`claim_to_headers` copies validated claims onto request headers for the backend; standard claims (`sub`, `iss`, `aud`, `exp`, `iat`, `nbf`, `jti`) and custom claims both work. `payload_in_metadata` stores the decoded claims on the request itself, which is how later filters — notably [Cedar policies](@/docs/security/cedar-policies.md) — see who the caller is.

```yaml,arion=fragment
providers:
  idp:
    # ...
    payload_in_metadata: jwt_payload
    claim_to_headers:
      - claim_name: sub
        header_name: x-jwt-sub
```

Validation details: `issuer` is checked when set, `audiences` when non-empty, and `clock_skew_seconds` adds leeway to `exp`/`nbf` checks. Decoded tokens are cached per worker, keyed by signature, so repeated requests skip signature verification.

## Verify

```bash
curl -i http://127.0.0.1:8080/          # 401, no token
curl -i http://127.0.0.1:8080/ -H "Authorization: Bearer $TOKEN"   # 200
```

Any failure — missing token, bad signature, wrong issuer or audience, expired — produces `401 Unauthorized` with a short reason in the body.

## Limits

- A rule requires exactly one `provider_name`. `requires_any`, `requires_all`, `allow_missing`, `allow_missing_or_failed` and `provider_and_audiences` are refused at startup.
- Named requirements (`requirement_map`) and `filter_state_rules` are not supported.
- `bypass_cors_preflight` is not supported — place the [CORS filter](@/docs/security/cors.md) before this one so preflights are answered without a token.
- There is no per-route override for this filter; route selection happens through `rules`.
- `require_expiration`, `max_lifetime` and the JWT cache size knob are not supported.
