---
title: "JWT authentication"
url: "/reference/filters/http/jwt-authn/"
kind: "reference"
summary: "HTTP filter that validates JSON Web Tokens against configured providers and JWKS keys."
---

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

# JWT authentication

HTTP filter that validates JSON Web Tokens against configured providers and JWKS keys.

| Attachment | Type URL (`@type`) | Per-route override | Build feature |
|---|---|---|---|
| HTTP filter chain (`http_filters`) | `type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication` | `disabled` only | None |

## What it does

The JWT filter extracts a token from the request, validates its signature against a provider's JWKS keys along with the issuer, audiences and expiry, and rejects failures with `401`. Which provider applies is decided by `rules` matched against the request path; validated claims are attached to the request for later filters ([Cedar](@/reference/filters/http/cedar-policy.md), the [MCP gateway](@/reference/filters/http/mcp-gateway.md)) and can be copied into headers.

## Minimal configuration

From `arion-runtime-jwt.yaml`; the fragment is an entry in `http_filters`, before the router:

```yaml,arion=fragment
- name: 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 "
        remote_jwks:
          http_uri:
            uri: https://idp.example.com/.well-known/jwks.json
            cluster: idp
            timeout: 5s
          cache_duration: 300s
    rules:
      - match: { prefix: "/" }
        requires: { provider_name: idp }
```

## Fields

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `providers` | map | Yes | — | Named providers; fields below. |
| `rules` | list | No | empty | Ordered rules. Each has an optional `match` (`prefix`, `path`, `safe_regex`, `path_separated_prefix`, plus `headers` and `query_parameters`) and `requires: {provider_name}`. The first matching rule picks the provider. |

> [!IMPORTANT]
> A request that resolves to no provider is rejected with `401` ("no provider found") — this includes requests matching no rule and rules written without `requires`. There is no `allow_missing` in Arion; `requires_any`, `requires_all`, `allow_missing`, `allow_missing_or_failed` and `requirement_map` names are unsupported and refused at startup.

### Provider fields

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `issuer` | string | No | — | Expected `iss` claim. |
| `audiences` | list | No | empty | Accepted `aud` values; empty accepts any. |
| `remote_jwks` / `local_jwks` | oneof | Yes | — | Key source. `remote_jwks` fetches `http_uri` (`uri`, `cluster`, `timeout`) and refreshes every `cache_duration` (default 60s), with an optional core `retry_policy`. `local_jwks` reads inline or from a file (`inline_string`, `filename`, `inline_bytes`, `environment_variable`). |
| `from_headers` | list | No | empty | Token locations: header `name` plus `value_prefix` to strip. There is no implicit default — a provider with no token source rejects every request. |
| `from_params` / `from_cookies` | list | No | empty | Query parameters or cookies to read the token from. |
| `forward` | bool | No | `false` | Keep the token on the forwarded request instead of stripping it. |
| `claim_to_headers` | list | No | empty | Copy claims to headers: `{header_name, claim_name}`. |
| `payload_in_metadata`, `header_in_metadata` | string | No | — | Store the decoded payload or header under this dynamic-metadata key. `payload_in_metadata` is what makes claims visible to Cedar and MCP RBAC. |
| `clock_skew_seconds` | uint32 | No | `0` | Tolerated clock skew for time-based claims. |
| `clear_route_cache` | bool | No | `false` | Recompute the route after the filter runs. |
| `subjects`, `forward_payload_header`, `failed_status_in_metadata` | — | No | — | Accepted at load but not enforced at runtime today. |

`require_expiration`, `max_lifetime`, `jwt_cache_config` and `normalize_payload_in_metadata` are unsupported and refused at startup.

## Ordering and interactions

Run JWT after [CORS](@/reference/filters/http/cors.md) (preflights carry no token) and before anything that consumes identity: RBAC on `claim_to_headers` output, [Cedar policies](@/reference/filters/http/cedar-policy.md) over claims, per-user rate limits, and MCP tool authorization. Per route the filter supports only the generic `FilterConfig` override with `disabled: true`.
