Implementing JWT Validation in Kong Plugins

When upstream services receive arbitrary bearer tokens that have bypassed gateway verification, every microservice must independently validate credentials — a pattern that multiplies attack surface and latency. Shifting JWT signature verification into the gateway’s access phase eliminates that duplication: Kong intercepts the request, resolves the consumer, verifies the cryptographic signature, validates configured claims, and only forwards requests that pass all checks. Failed tokens are rejected at the edge with a 401 response that never reaches your service mesh.

Prerequisite Concepts

This page covers a specific Kong 3.x configuration scenario. Before continuing, make sure you are comfortable with the broader topic of authentication proxying and token validation at the gateway layer, including how OAuth2 introspection compares to local JWT verification. The placement of this validation step inside Kong’s middleware chain determines execution order relative to rate limiting and throttling, request transformation, and logging plugins — understanding that ordering prevents subtle policy gaps.

How Kong Resolves and Verifies a JWT

The built-in jwt plugin operates entirely in the access phase. The sequence below shows exactly what happens between the client sending a token and Kong forwarding — or terminating — the request.

Kong JWT Validation Sequence Sequence diagram showing how Kong's jwt plugin processes a bearer token: extraction, credential lookup, signature verification, claim validation, and upstream forwarding or 401 rejection. Client Kong (access) Upstream GET /api — Authorization: Bearer <token> Extract + decode header Lookup key_claim_name value Verify signature (RS256/HS256) Validate exp, nbf claims Forward + X-Consumer-* headers 200 OK (upstream response) 401 — signature or claim failure success path failure path

Kong injects X-Consumer-ID, X-Consumer-Username, and X-Consumer-Custom-ID headers into the forwarded request so upstream services can read consumer identity without ever seeing the raw token.

Step-by-Step Configuration for Kong 3.x

1. Create a Consumer

Every JWT credential is attached to a Kong consumer. Create the consumer first:

# Kong 3.x Admin API
curl -i -X POST http://localhost:8001/consumers \
  --data username=my-api-client \
  --data custom_id=client-001

2. Create the JWT Credential

The algorithm field on the credential — not on the plugin — controls which algorithm Kong accepts for this consumer. Create credentials only for the algorithm your identity provider actually uses.

For HS256 (shared secret):

curl -i -X POST http://localhost:8001/consumers/my-api-client/jwt \
  --data algorithm=HS256 \
  --data key=https://my-idp.example.com \
  --data secret=REPLACE_WITH_RANDOM_SECRET_MIN_32_CHARS

For RS256 (public-key signature):

curl -i -X POST http://localhost:8001/consumers/my-api-client/jwt \
  --data algorithm=RS256 \
  --data key=https://my-idp.example.com \
  --data "rsa_public_key=$(cat public.pem)"

The key field must exactly match the value your identity provider places in the claim named by key_claim_name (typically iss). A mismatch here produces an immediate 401 with no informative error in the response body — check error.log instead.

3. Attach the Plugin to a Route (Declarative — Kong 3.x)

Route-level attachment limits enforcement to routes that require bearer token auth, leaving public health-check or webhook routes untouched:

# kong.yaml — declarative format (Kong 3.x)
_format_version: "3.0"

services:
  - name: my-api-service
    url: http://upstream-service:8080

routes:
  - name: my-api-route
    service: my-api-service
    paths:
      - /api

plugins:
  - name: jwt
    route: my-api-route
    config:
      key_claim_name: iss          # claim used to look up the credential
      claims_to_verify:
        - exp                      # token must not be expired
        - nbf                      # token must not be used before its valid-from time
      secret_is_base64: false      # true only if the HS256 secret was base64-encoded at storage time
      run_on_preflight: false      # skip validation on OPTIONS — preserves CORS handshakes
      maximum_expiration: 3600     # reject tokens with exp more than 1h in the future
      clock_skew: 30               # tolerate 30s NTP drift across distributed nodes

Configuration field reference:

Field Type Purpose
key_claim_name string JWT claim whose value selects the credential record. Typically iss or kid.
claims_to_verify array Claims Kong must validate. exp and nbf are the only built-in options.
secret_is_base64 boolean Set true only when the HS256 secret was base64-encoded before being stored in Kong’s database. Ignored for RS256.
run_on_preflight boolean When false, OPTIONS requests bypass signature checks. Recommended false for REST APIs with CORS.
maximum_expiration integer Reject tokens whose exp minus current time exceeds this value (seconds). 0 disables the check.
clock_skew integer Leeway in seconds for exp/nbf comparisons. Use 30 for typical distributed deployments.

4. Service-Level Attachment (Apply to All Routes)

Attaching the plugin at the service level enforces validation on every route under that service — use this when you want uniform token requirements across the entire backend:

plugins:
  - name: jwt
    service: my-api-service
    config:
      key_claim_name: iss
      claims_to_verify:
        - exp
      clock_skew: 30

Route-level plugins override service-level plugins for the same plugin type, so you can still exempt individual routes (e.g., /api/health) by attaching a separate plugin configuration at the route level.

Decision Matrix: Route-Level vs Service-Level vs Global

Scope When to use Trade-off
Route-level Most API routes require auth but some (health, webhook callbacks) must remain open Explicit per-route control; more configuration surface area
Service-level Every route under a backend must require a valid JWT Simpler config; harder to exempt individual routes without per-route overrides
Global Organisation-wide: every service and route behind Kong must present a token Maximum enforcement; breaks public endpoints unless they bypass Kong or use a separate route group

Gotchas and Failure Signals

Algorithm confusion. Never create both HS256 and RS256 credentials for the same consumer unless both are genuinely required. Kong resolves credentials by matching the key_claim_name claim value to the credential’s key field, then verifies the signature using the matched credential’s algorithm. A token crafted with the public RSA key as an HMAC secret could satisfy an HS256 credential check. Restrict each consumer to one algorithm.

key_claim_name mismatch. If the claim named by key_claim_name is absent from the JWT payload, Kong returns 401 with no credential-resolution detail in the client response. The exact failure is logged as jwt: no mandatory 'iss' in claims (substituting whichever claim name you configured). Cross-check the JWT payload using jwt.io before debugging Kong configuration.

PEM formatting for RS256. The rsa_public_key field must contain a standard PEM block with \n line separators and correct header/footer (-----BEGIN PUBLIC KEY-----). Trailing whitespace or Windows CRLF line endings cause signature verification to fail silently with a generic 401.

secret_is_base64 encoding mismatch (HS256). If your token was issued with the raw secret but Kong was configured with secret_is_base64: true, Kong base64-decodes the stored secret before HMAC computation — producing a different key than the issuer used. The jwt: invalid signature error in the Kong error log distinguishes this from a credential-resolution failure.

maximum_expiration and long-lived tokens. Setting maximum_expiration: 0 disables the upper-bound check entirely. For machine-to-machine tokens that are legitimately long-lived, set this to match your organisation’s maximum allowed token lifetime (e.g., 86400 for 24h) to catch misconfigured issuers that emit tokens with arbitrarily distant exp values.

clock_skew abuse. Increasing clock_skew beyond 60 seconds to “fix” expired-token errors masks legitimately expired tokens. Instead, fix the issuer’s clock or refresh-token logic. Use clock_skew only to accommodate real NTP drift between distributed Kong nodes.

Log isolation during debugging:

# Stream only jwt-plugin errors from Kong's error log
tail -f /usr/local/kong/logs/error.log | grep '"plugin":"jwt"'

Kong 3.x structured logs emit "plugin":"jwt" in the log line for plugin-phase errors, making it straightforward to filter against other plugin failures.

Validation Checklist

  • Consumer exists and has at least one JWT credential with the correct algorithm field
  • Credential key field matches the value the identity provider places in the key_claim_name claim
  • RS256 credentials supply a valid, newline-separated PEM public key in rsa_public_key
  • HS256 credentials have secret_is_base64 matching the actual encoding used at credential creation time
  • Plugin is attached at the correct scope (route, service, or global) with no unintended public-endpoint exposure
  • claims_to_verify includes at minimum exp for all non-machine token flows
  • maximum_expiration is set to a non-zero value aligned with your token lifetime policy
  • run_on_preflight: false is set for any route that accepts cross-origin browser requests
  • clock_skew is <= 60 seconds and not used to paper over expired-token issues
  • No consumer has both HS256 and RS256 credentials unless explicitly required

FAQ

Does Kong’s built-in jwt plugin support RS256?

Yes. Create the consumer credential with algorithm: RS256 and supply the PEM-encoded public key in the rsa_public_key field. The algorithm field on the plugin config itself does not exist — algorithm enforcement is per-credential, not per-plugin instance.

How do I prevent algorithm confusion attacks in Kong?

Never create both HS256 and RS256 credentials for the same consumer unless both algorithms are genuinely supported. Kong resolves credentials by the key_claim_name value, so a token signed with HS256 using the public key as the secret could match an RS256 credential. Restrict each consumer to one algorithm type.

What HTTP status does Kong return on JWT validation failure?

Kong returns 401 Unauthorized for missing or structurally invalid tokens, expired tokens, and signature mismatches. It returns 403 Forbidden when the token is cryptographically valid but the consumer lacks access to the matched route.


Related