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 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
algorithmfield - Credential
keyfield matches the value the identity provider places in thekey_claim_nameclaim - RS256 credentials supply a valid, newline-separated PEM public key in
rsa_public_key - HS256 credentials have
secret_is_base64matching 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_verifyincludes at minimumexpfor all non-machine token flows -
maximum_expirationis set to a non-zero value aligned with your token lifetime policy -
run_on_preflight: falseis set for any route that accepts cross-origin browser requests -
clock_skewis<= 60seconds 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
- Authentication Proxying & Token Validation — parent topic covering OAuth2 introspection, API key validation, and mTLS alongside JWT patterns
- Routing by API Key vs JWT Claims — decision guide for choosing between credential types at the routing layer
- Configuring CORS Policies for Multi-Tenant APIs — CORS preflight interaction with authentication plugins
- Middleware Chains & Request Transformation — plugin execution order, phase model, and transformation patterns