The Ultimate Guide of Validating JWT With Kong

I understand that describing this post as the “ultimate” guide may sound ambitious, but my goal is to present a thorough exploration of all the official Kong plugins available for JWT validation. After you’ve completed this read, you’ll possess a comprehensive grasp of the options within your reach. Armed with this knowledge, you’ll be well-equipped to make an informed choice, selecting the plugin that perfectly suits your particular use case.

Introduction

JSON Web Tokens (JWTs) have become integral in the world of web development, serving as carriers of vital information across systems. In the quest to ensure the authenticity of requests, the validation of JWT tokens becomes a crucial step. Simultaneously, API gateways like Kong hold a pivotal role in an organization’s API architecture, particularly in the rapidly advancing adoption of Oauth 2.0 and OIDC standards. In this blog post, we’ll delve into the tools available for validating JWTs with Kong, exploring the critical components of this process in today’s dynamic web development landscape.

What is JWT

Feel free to skip and jump to the next section to learn which plugin you should be using if you are already familiar with JWT.

JSON Web Token (JWT) is an abstract that is represented in the form of JSON Web Signature (JWS) and JSON Web Encryption (JWE). To keep this post concise, when we refer to JWTs here, we are primarily focusing on JWS. If you are interested in knowing validating JWE, please check this post.

According to RFC7515 JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures.. In more straightforward terms, picture it as a letter containing a crucial message, and this message carries the signature of a specific individual. To guarantee the letter’s authenticity, we must validate that signature.

Algorithm

Cryptographic algorithms and identifiers for use with JWS are described in the separate JSON Web Algorithms (JWA) JWA specification and an IANA registry defined by that specification.

The cryptographic algorithms and identifiers intended for use with JWS are outlined in the distinct JSON Web Algorithms (JWA) JWA specification.

Typically, it is recommended to employ asymmetric algorithms such as RS256 or ES256. The use of symmetric algorithms like HS256 is discouraged.

Structure

Let’s take a look at JWTs structure first. Here is an example:

eyJhbGciOiJFUzI1NiIsImtpZCI6Imh1SEE3RDVaTUNKTWhLaVJIZVgwaGZSWG9fX1VBbEpCZ0FkTjhxb0MwMXcifQ.eyJpc3MiOiAiZm9tbSIsICJhdWQiOiAiand0LWRlbW8iLCAic3ViIjogImRlbW8tdXNlciIsICJpYXQiOiAxNjcyNDAzMzc4LCAiZXhwIjogMTY3MjQwMzY3OH0.bxkLGEjN4pXQQ6eymBO_DYl24NGu07FFR1ZXgmdFYHPGsNX10r6iyqDEtCHeXWs7Hsn-QIasV_i4Lw2nCHmlAA
  • JOSE header
    JOSE header normally tells the recipient the hashing algorithm (alg) and key id (kid) that are used to secure this JWS. It is also very common to see type ( typ).

    1
    2
    3
    4
    {
    "alg": "ES256",
    "kid": "huHA7D5ZMCJMhKiRHeX0hfRXo__UAlJBgAdN8qoC01w"
    }
  • Payload
    Payload is the message to be secured and passed to the other party.

    1
    2
    3
    4
    5
    6
    7
    {
    "iss": "fomm",
    "aud": "jwt-demo",
    "sub": "demo-user",
    "iat": 1672403378,
    "exp": 1672403678
    }
  • Signature
    Users need to compute signature of protected header and payload using algorithm defined in JOSE header and make sure signature matches.

Generate JWT

Given the popularity of JWT, there’s libraries for different languages at your disposal. If you’re utilizing OIDC providers such as Keycloak or Azure, they will automatically generate JWT tokens for you. In this section, we will demonstrate how to create JWTs using jwt-cli and Python jwcrypto library.

Generate RSA Key pair

Let me use openssl to generate RSA private key for signing our tokens and export its public key for validation.

1
2
3
4
5
6
7
8
openssl genpkey \
-algorithm RSA \
-pkeyopt rsa_keygen_bits:2048 \
-outform pem -out rsa-private.pem 2>/dev/null

openssl pkey -pubout \
-in rsa-private.pem \
-out rsa-public.pem

JWT CLI

If you have nix install, you can use nix shell nixpkgs#jwt-cli to use it. Otherwise please refer to official git repo.

kid

According to RFC 7415 section 4.1.4, The structure of the kid value is unspecified. Its value MUST be a case-sensitive string. Use of this Header Parameter is OPTIONAL. Although it is optional, in most cases you would want to include kid in the JOSE Header, this helps the recipient to identify which public key from your JWK can be used to validate the token.

I will use a tool to generate kid so it can be linked back to this specific public key.

1
2
3
4
cat rsa-public.pem \
| docker run --rm -i danedmunds/pem-to-jwk:latest --jwks-out \
| jq -r '.keys[].kid' \
| read JWT_KID

JWT

Now we can generate JWT as below with a simple payload.

1
2
3
4
5
6
7
jwt encode \
--alg RS256 \
--exp=300s \
--kid $JWT_KID \
--iss fomm-jwt-cli \
--secret @rsa-private.pem \
'{"username":"fomm","roles":["demo"]}'

Validate JWT

Because we use RSA private key to sign our token, we need to use its PUBLIC KEY to validate. With jwt-cli, we can validate with below command.

1
jwt decode --secret @rsa-public.pem --alg RS256 <JWT>

Python

Environment

You can install it with pip pip install PyJWT. I prefer to use nix develop. Assuming you have nix install you can save below to flake.nix. Then you can run nix develop -c $SHELL to get the environment set up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
description = "Example Python development environment for Zero to Nix";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
};
outputs = {
self,
nixpkgs,
}: let
# Systems supported
allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
"aarch64-linux" # 64-bit ARM Linux
"x86_64-darwin" # 64-bit Intel macOS
"aarch64-darwin" # 64-bit ARM macOS
];
forAllSystems = f:
nixpkgs.lib.genAttrs allSystems (system:
f {
pkgs = import nixpkgs {inherit system;};
});
in {
# Development environment output
devShells = forAllSystems ({pkgs}: {
default = let
# Use Python 3.11
python = pkgs.python311;
in
pkgs.mkShell {
# The Nix packages provided in the environment
packages = [
# Python plus helper tools
(python.withPackages (ps:
with ps; [
jwcrypto
]))
];
};
});
};
}

JWT

Now let’s save below to jwt.py and we should be able to run python jwt.py to create a token.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from jwcrypto import jwk, jwt
from datetime import datetime

def generate_jwt(private_key_path, algorithm):
timestamp = int(datetime.now().timestamp())
payload = {
"iat": timestamp,
"exp": timestamp + 300,
"iss": "fomm-jwtcrypto",
"username": "fomm",
"roles": ["demo"],
}
with open(private_key_path, "rb") as pemfile:
private_key = jwk.JWK.from_pem(pemfile.read())
jwt_token = jwt.JWT(header={"alg": algorithm, "kid": private_key.thumbprint(), "typ":"JWT"}, claims=payload)
jwt_token.make_signed_token(private_key)
return jwt_token.serialize()

def main():
private_key_path = "rsa-private.pem"
jwt_algorithm = "RS256"
jwt_token = generate_jwt(private_key_path, jwt_algorithm)
print(jwt_token)

if __name__ == "__main__":
main()

Validate JWT

Validation with jwcrypto library is pretty straight forward. Here is a sample you can use.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from jwcrypto import jwk, jwt

def validate_jwt(public_key_path, jwt_token):
with open(public_key_path, "rb") as pemfile:
public_key = jwk.JWK.from_pem(pemfile.read())
verified_token = jwt.JWT(jwt=jwt_token, key=public_key)
try:
verified_token.validate(public_key)
return True
except Exception as e:
return False

def main():
public_key_path = "rsa-public.pem"
jwt_token = input("Enter JWT token: ")
is_valid = validate_jwt(public_key_path, jwt_token)
if is_valid:
print("Token is valid.")
else:
print("Token is invalid.")

if __name__ == "__main__":
main()

You can save the content to jwt-validate.py, run it and follow the prompt to provide your JWT.

Which Plugin should you use?

There are 3 official plugins are available for JWT validation.

  • JWT plugin
  • JWT signer plugin (Enterprise)
  • OpenID Connect aka OIDC plugin (Enterprise)

If you do not have an Enterprise license, your can only use the open-source JWT plugin. To implement this plugin, you’ll need to create individual jwt_secrets for each consumer. In cases where you need to associate the same RSA public key with different consumers, ensure that the key value in all jwt_secrets is unique. You can check out my blog post on this plugin here.

If you have access to all three plugins, here are the questions to consider:

  • How do you generate JWTs?

    • If you rely on an IDP to generate tokens, it is better to use OIDC and JWT Signer.
  • Do you need to validate JWTs from multiple IDPs?

    • OIDC Plugin supports fetching and auto rotate public keys from multiple JWKs and use these keys for JWT validation.
  • Do you have network restriction on the upstream API services that do not have internet access?

    • You can use the JWT Signer plugin to re-sign tokens and your upstream servers only need to trust Kong’s public key, eliminating the need to fetching IDPs JWKs.
  • Do you need to validate JWT claims?

    • If scope validation is necessary, OIDC is the preferred choice. It supports the validation of up to four claims simultaneously.
  • Do you need to read token claim and then pass it to upstream or downstream header?

    • If this need arises, you must use OIDC plugin.
  • Do you need to validate two token issued by different IDPs at the same time?

    • In the event your API consumers need to validate their requests with two tokens, such as an access token and a channel token, the JWT Signer plugin should be your selection.
  • Do you need Group mapping for access control?

    • All three plugins allow for consumer mapping. However, if you wish to manage developers on IDPs, you can use the OIDC plugin to create virtual credentials based on the token claims. These virtual credentials can be utilized for rate limiting and access control.

If you’re still unsure about which plugin to choose, my recommendation is to start with OIDC. It’s Kong’s most complex authentication plugin, but it provides a wealth of additional features compared to the other two, including key rotation rediscovery, virtual credentials, scope validation, and more.

You might be wondering, “Considering your high praise for the OIDC plugin, can I still use it if I don’t use any OpenID Connect provider?” The answer is yes, and I’ll guide you through the process.

OIDC JWT validation

The way it work is really quite simple. The OIDC plugin is responsible of fetching JWKs, these public keys will be stored in cached and used to validate JWTs. When IDC rotates their keys and sign tokens using new keys, OIDC plugin will re-discovery the public keys from JWK endpoints. You can check which public keys Kong has by visiting <admin_api>/openid-connect/issuers endpoint if you have access to Admin API

JWT signer works similarly except it can only fetch JWKs from one URL and it also re-sign the token. Check out this blog post to learn more about JWT signer plugin.

Generate JWKs

We use danedmunds/pem-to-jwk docker image to generate the public key JWK.

1
2
3
4
cat rsa-public.pem \
| docker run -i danedmunds/pem-to-jwk:latest --jwks-out \
| jq '.keys[] += {alg:"RS256"}' \
> jwks.json

Create docker network

We will run our containers in the same docker network kong-net.

1
docker network create kong-net

Host JWKs

Next I will use a json-server to host these keys.

1
2
3
4
5
6
docker run --rm \
--detach --name jwk \
--network kong-net \
-p "3000:3000" \
--mount type=bind,src="$(pwd)"/jwks.json,dst=/jwk.json \
williamyeh/json-server --watch /jwk.json

Start Kong EE

As usual, I will deploy Kong in the DBless mode for our demo. Assuming you have a valid kong enterprise license and it is stored in environment variable KONG_LICENSE_DATA. We can start Kong Enterprise 3.5 in dbless mode with below command.

1
2
3
4
5
6
7
8
docker run --rm --detach --name kong \
--network kong-net \
-p "8000-8001:8000-8001" \
-e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
-e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
-e "KONG_DATABASE=off" \
-e "KONG_LICENSE_DATA=$KONG_LICENSE_DATA" \
kong/kong-gateway:3.5

Prepare Kong config

Let’s save below config to /tmp/kong.yaml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
_format_version: "3.0"
routes:
- name: upstream-route
paths:
- /upstream
plugins:
- config:
echo: true
status_code: 200
name: request-termination
services:
- name: test-svc
host: localhost
path: /upstream
port: 8000
protocol: http
routes:
- name: test-route
paths:
- /test
plugins:
- config:
auth_methods:
- bearer
extra_jwks_uris:
- http://jwk:3000/db
issuer: http://jwk:3000/db
issuers_allowed:
- fomm-jwt-cli
- fomm-jwtcrypto
name: openid-connect

Here is what I do with above config.

  1. I use request termination plugin as upstream to return 200 response and echo back the request.
  2. I make sure all my JWKs under config.extra_jwks_uris so the OIDC plugin can fetch the keys.
  3. All issuers (my python and jwt-cli commands) are listed under config.issuers_allowed. OIDC plugin validates issuers by default.

Then we can push this file to kong at /config endpoint.

1
2
3
curl --request POST \
--url http://localhost:8001/config \
-F config=@/tmp/kong.yaml

Validate JWT

Now we have everything ready, let’s test our setup. You can follow above steps to generate a JWT first. For a quick reference, below command store the JWT in token variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
cat rsa-public.pem \
| docker run -i danedmunds/pem-to-jwk:latest --jwks-out \
| jq -r '.keys[].kid' \
| read JWT_KID

jwt encode \
--alg RS256 \
--exp=300s \
--kid $JWT_KID \
--iss fomm-jwt-cli \
--secret @rsa-private.pem \
'{"username":"fomm","roles":["demo"]}' \
| read token

Next we can call our endpoint with this token as authorization bearer token and we should get a 200 response.

1
2
3
curl --request GET -i \
--url http://localhost:8000/test \
--header "authorization: Bearer $token"

That’s all I want to share with you today, hopefully it helps you to choose the right plugin.

See you next time