<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Emmanuel Chukwudi</title>
    <description>The latest articles on Forem by Emmanuel Chukwudi (@emmsddev).</description>
    <link>https://forem.com/emmsddev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3618182%2F188990d5-c2db-4fb7-b510-34288de5cf77.jpg</url>
      <title>Forem: Emmanuel Chukwudi</title>
      <link>https://forem.com/emmsddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/emmsddev"/>
    <language>en</language>
    <item>
      <title>Validate JWTs from Multiple Issuers in kgateway</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Sun, 17 May 2026 07:52:37 +0000</pubDate>
      <link>https://forem.com/emmsddev/validate-jwts-from-multiple-issuers-in-kgateway-561f</link>
      <guid>https://forem.com/emmsddev/validate-jwts-from-multiple-issuers-in-kgateway-561f</guid>
      <description>&lt;p&gt;Production APIs often need to accept tokens from more than one identity provider for example, a tenant's own Auth0 tenant &lt;em&gt;and&lt;/em&gt; Google Workspace for internal tools. kgateway's &lt;code&gt;JWTPolicy&lt;/code&gt; resource lets you declare multiple issuers in one policy and attach it to any &lt;code&gt;HTTPRoute&lt;/code&gt;, so you don't need a separate gateway per IdP.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a JWT?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;JSON Web Token (JWT)&lt;/strong&gt; 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Think of it like a signed event wristband.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Structure of a JWT
&lt;/h3&gt;

&lt;p&gt;A JWT is three Base64URL-encoded JSON objects joined by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9   ← Header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20i...  ← Payload
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it contains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Header&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Algorithm (&lt;code&gt;RS256&lt;/code&gt;) and token type. Tells the verifier how to check the signature.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claims about the user and the token who issued it, who it's for, when it expires.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signature&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cryptographic proof the token hasn't been tampered with. Verified against the IdP's public key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can paste any JWT into &lt;a href="https://jwt.io" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt; to decode it instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's inside the payload?
&lt;/h3&gt;

&lt;p&gt;The payload is a JSON object of &lt;strong&gt;claims&lt;/strong&gt; statements about the token and its subject. Some are standard; some are custom fields added by your IdP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://my-tenant.auth0.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;issuer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;who&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;created&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unique&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audience&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1717000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;expiry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;claim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;added&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auth&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;claim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RBAC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How signature verification works (JWKS)
&lt;/h3&gt;

&lt;p&gt;JWTs signed with RS256 use asymmetric cryptography: the IdP signs tokens with a &lt;strong&gt;private key&lt;/strong&gt; that only it holds, and publishes the corresponding &lt;strong&gt;public keys&lt;/strong&gt; at a well known URL called the &lt;strong&gt;JWKS endpoint&lt;/strong&gt; (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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters for multi-issuer setups:&lt;/strong&gt; 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 &lt;code&gt;iss&lt;/code&gt; claim first.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;HTTPRoute&lt;/code&gt; on &lt;code&gt;/api&lt;/code&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts RS256-signed JWTs from &lt;strong&gt;Auth0&lt;/strong&gt; and &lt;strong&gt;Google&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enforces &lt;code&gt;aud: my-api&lt;/code&gt; on tokens from both providers&lt;/li&gt;
&lt;li&gt;Forwards the &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; claims as &lt;code&gt;X-User-Id&lt;/code&gt; and &lt;code&gt;X-User-Email&lt;/code&gt; headers to your upstream service&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;kgateway ≥ 1.2 installed in your cluster&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; access with permissions to create custom resources&lt;/li&gt;
&lt;li&gt;An Auth0 tenant with an API audience configured&lt;/li&gt;
&lt;li&gt;A Google OAuth 2.0 client ID&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How kgateway validates JWTs
&lt;/h2&gt;

&lt;p&gt;Validation happens in the Envoy data plane &lt;strong&gt;before&lt;/strong&gt; a request ever reaches your upstream. On each request, kgateway:&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Step 1: Create the JWTPolicy
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;JWTPolicy&lt;/code&gt; 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 &lt;code&gt;jwt-policy.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.kgateway.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JWTPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multi-issuer-policy&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# Provider 1: Auth0 tenant&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auth0&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://my-tenant.auth0.com/&lt;/span&gt;     &lt;span class="c1"&gt;# note the trailing slash&lt;/span&gt;
      &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
      &lt;span class="na"&gt;remoteJwks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://my-tenant.auth0.com/.well-known/jwks.json&lt;/span&gt;
        &lt;span class="na"&gt;cacheDuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
      &lt;span class="na"&gt;claimsToHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Email&lt;/span&gt;

    &lt;span class="c1"&gt;# Provider 2: Google&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://accounts.google.com&lt;/span&gt;      &lt;span class="c1"&gt;# no trailing slash&lt;/span&gt;
      &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
      &lt;span class="na"&gt;remoteJwks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.googleapis.com/oauth2/v3/certs&lt;/span&gt;
        &lt;span class="na"&gt;cacheDuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
      &lt;span class="na"&gt;claimsToHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Issuer strings must be exact.&lt;/strong&gt; The &lt;code&gt;issuer&lt;/code&gt; field is compared character-for-character against the token's &lt;code&gt;iss&lt;/code&gt; 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.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Attach the policy to your HTTPRoute
&lt;/h2&gt;

&lt;p&gt;Reference the policy via an annotation on your &lt;code&gt;HTTPRoute&lt;/code&gt;. You do not need to modify the route's rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPRoute&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-route&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway.kgateway.dev/jwt-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multi-issuer-policy&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parentRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-gateway&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PathPrefix&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api&lt;/span&gt;
      &lt;span class="na"&gt;backendRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-service&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Apply and verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply both resources&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; jwt-policy.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; httproute.yaml

&lt;span class="c"&gt;# Confirm the policy is accepted by the control plane&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get jwtpolicy multi-issuer-policy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.conditions[?(@.type=="Ready")].status}'&lt;/span&gt;
True

&lt;span class="c"&gt;# Test with a valid Auth0 token&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AUTH0_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://my-gateway/api/health
200 OK

&lt;span class="c"&gt;# Test rejection: no token → 401&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://my-gateway/api/health
401 Unauthorized

&lt;span class="c"&gt;# Confirm upstream receives the forwarded headers&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl logs deploy/my-service | &lt;span class="nb"&gt;grep &lt;/span&gt;X-User-Id
X-User-Id: user_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  Claim validation reference
&lt;/h2&gt;

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




&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

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

</description>
      <category>kgateway</category>
      <category>jwt</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
  </channel>
</rss>
