DEV Community

Cover image for Validate JWTs from Multiple Issuers in kgateway
Emmanuel Chukwudi
Emmanuel Chukwudi

Posted on

Validate JWTs from Multiple Issuers in kgateway

Production APIs often need to accept tokens from more than one identity provider for example, a tenant's own Auth0 tenant and Google Workspace for internal tools. kgateway's JWTPolicy resource lets you declare multiple issuers in one policy and attach it to any HTTPRoute, so you don't need a separate gateway per IdP.

This guide walks through a working, reproducible configuration. By the end you will have a policy that validates tokens from two issuers, rejects mismatched audiences, and forwards selected claims as upstream headers.


What is a JWT?

A JSON Web Token (JWT) is a compact, self-contained credential that an identity provider (IdP) issues to a user or service after they authenticate. Instead of your API checking a username and password on every request, the client attaches a JWT and your API trusts it because it was cryptographically signed by someone it already trusts.

Think of it like a signed event wristband. The venue (IdP) checks your ID once at the gate and gives you a wristband. Staff inside the venue (your APIs) can verify the wristband is genuine without phoning the front gate again. The wristband also says which areas you can access and it expires at midnight.

Structure of a JWT

A JWT is three Base64URL-encoded JSON objects joined by dots:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9   ← Header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20i...  ← Payload
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature
Enter fullscreen mode Exit fullscreen mode
Part What it contains
Header Algorithm (RS256) and token type. Tells the verifier how to check the signature.
Payload Claims about the user and the token who issued it, who it's for, when it expires.
Signature Cryptographic proof the token hasn't been tampered with. Verified against the IdP's public key.

You can paste any JWT into jwt.io to decode it instantly.

What's inside the payload?

The payload is a JSON object of claims statements about the token and its subject. Some are standard; some are custom fields added by your IdP.

{
  "iss": "https://my-tenant.auth0.com/",  // issuer  who created the token
  "sub": "user_123",                       // subject  the user's unique ID
  "aud": "my-api",                         // audience  which service this token is for
  "exp": 1717000000,                       // expiry  Unix timestamp
  "email": "alice@example.com",            // custom claim added by Auth0
  "roles": ["admin", "editor"]             // custom claim for RBAC
}
Enter fullscreen mode Exit fullscreen mode

How signature verification works (JWKS)

JWTs signed with RS256 use asymmetric cryptography: the IdP signs tokens with a private key that only it holds, and publishes the corresponding public keys at a well known URL called the JWKS endpoint (JSON Web Key Set). Anyone including kgateway can fetch these public keys and verify that a token was genuinely issued by that IdP and hasn't been altered since.

This means kgateway never needs to call back to your IdP on every request. It fetches the JWKS once, caches the keys, and verifies signatures locally at the data plane making validation fast and offline capable.

Why this matters for multi-issuer setups: Each IdP has its own JWKS endpoint and its own signing keys. kgateway can hold keys from multiple providers simultaneously, matching each incoming token to the right key by checking its iss claim first.


What you'll build

An HTTPRoute on /api that:

  • Accepts RS256-signed JWTs from Auth0 and Google
  • Enforces aud: my-api on tokens from both providers
  • Forwards the sub and email claims as X-User-Id and X-User-Email headers to your upstream service

Before you begin

  • kgateway ≥ 1.2 installed in your cluster
  • kubectl access with permissions to create custom resources
  • An Auth0 tenant with an API audience configured
  • A Google OAuth 2.0 client ID

How kgateway validates JWTs

Validation happens in the Envoy data plane before a request ever reaches your upstream. On each request, kgateway:

  1. Extracts the bearer token from the Authorization: Bearer <token> header (configurable to cookies or query params).
  2. Resolves the matching issuer by comparing the token's iss claim against each issuer declared in JWTPolicy.spec.providers. The first match wins.
  3. Fetches and caches JWKS from the provider's jwks_uri. Keys are cached per the cacheDuration you set and never re-fetched mid-request.
  4. Validates claims and signature verifies exp, nbf, aud, and the cryptographic signature. Any failure returns 401 Unauthorized immediately.
  5. Forwards claims as headers injects declared claims into request headers so your upstream can make authorization decisions without reparsing the JWT.

Step 1: Create the JWTPolicy

The JWTPolicy is a namespace scoped custom resource that declares which issuers to trust, where to fetch their public keys, and which claims to forward upstream. Create a file named jwt-policy.yaml:

apiVersion: gateway.kgateway.dev/v1alpha1
kind: JWTPolicy
metadata:
  name: multi-issuer-policy
  namespace: default
spec:
  providers:

    # Provider 1: Auth0 tenant
    - name: auth0
      issuer: https://my-tenant.auth0.com/     # note the trailing slash
      audiences:
        - my-api
      remoteJwks:
        uri: https://my-tenant.auth0.com/.well-known/jwks.json
        cacheDuration: 10m
      claimsToHeaders:
        - claim: sub
          header: X-User-Id
        - claim: email
          header: X-User-Email

    # Provider 2: Google
    - name: google
      issuer: https://accounts.google.com      # no trailing slash
      audiences:
        - my-api
      remoteJwks:
        uri: https://www.googleapis.com/oauth2/v3/certs
        cacheDuration: 5m
      claimsToHeaders:
        - claim: sub
          header: X-User-Id
        - claim: email
          header: X-User-Email
Enter fullscreen mode Exit fullscreen mode

⚠️ Issuer strings must be exact. The issuer field is compared character-for-character against the token's iss claim. Auth0 includes a trailing slash in its tokens; Google does not. A mismatch here means every token from that provider will be rejected, even if the signature is valid.


Step 2: Attach the policy to your HTTPRoute

Reference the policy via an annotation on your HTTPRoute. You do not need to modify the route's rules:

apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: default
  annotations:
    gateway.kgateway.dev/jwt-policy: multi-issuer-policy
spec:
  parentRefs:
    - name: my-gateway
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: my-service
          port: 8080
Enter fullscreen mode Exit fullscreen mode

Step 3: Apply and verify

# Apply both resources
$ kubectl apply -f jwt-policy.yaml -f httproute.yaml

# Confirm the policy is accepted by the control plane
$ kubectl get jwtpolicy multi-issuer-policy \
    -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
True

# Test with a valid Auth0 token
$ curl -H "Authorization: Bearer $AUTH0_TOKEN" https://my-gateway/api/health
200 OK

# Test rejection: no token → 401
$ curl https://my-gateway/api/health
401 Unauthorized

# Confirm upstream receives the forwarded headers
$ kubectl logs deploy/my-service | grep X-User-Id
X-User-Id: user_123
Enter fullscreen mode Exit fullscreen mode

⚠️ JWKS caching on first request: kgateway fetches JWKS the first time a token from a given issuer arrives. If the jwks_uri is unreachable at that moment, the request fails with 503. Use a cacheDuration of at least 5m in production never 0s outside of development.


Claim validation reference

Claim Validated automatically Notes
iss Yes Must exactly match a declared provider's issuer. First match wins; no fallback.
aud Yes, if configured Token must contain at least one value from the audiences list. Omit the field to skip audience validation (not recommended in production).
exp Yes Expired tokens are rejected with 401. Clock skew tolerance is 60 s by default.
nbf Yes Tokens with a future nbf (not-before) are rejected.
sub, email, roles No kgateway does not validate custom claims. Use claimsToHeaders to forward them and enforce access rules in your upstream service.

Next steps

  • Use a local JWKS secret: Mount JWKS as a Kubernetes secret for air gapped or high security environments.
  • Claim based routing: Route requests to different backends based on forwarded claim headers.
  • Full OIDC with Auth0: Add the authorization code flow for browser facing applications.
  • Monitor validation errors: Surface JWT rejection rates in Prometheus and set alerting thresholds.

Top comments (0)